Workers y Containers: el front y el backend pesado
El patrón que hace útiles a los Containers: el Worker como front barato, instantáneo y global que enruta, autentica y cachea; el Container como backend potente pero escaso, facturado por tiempo, al que solo se delega el trabajo irreducible. Vemos cómo el Worker dirige peticiones a una instancia con getContainer, por qué puede resolver sin despertar al contenedor, y el criterio para decidir cuándo un contenedor está justificado y cuándo es un antipatrón.
Un Container casi nunca se usa solo: se usa detrás de un Worker. El Worker es el front —barato, instantáneo, desplegado en toda la red— y el Container es el backend pesado —potente pero escaso, facturado por el tiempo que corre—. El arte de esta arquitectura está en dónde trazas la línea: cuánto trabajo resuelve el front antes de molestar al backend, y qué porción mínima merece de verdad descender a un sistema operativo entero. Dominar ese reparto es la diferencia entre una app que aprovecha el edge y otra que arrastra un servidor caro disfrazado.
- Distinguir las dos economías: el Worker como front barato y global, el Container como backend facturado por tiempo.
- Aplicar el patrón “Worker delante”: enrutar, autenticar y cachear arriba; delegar abajo.
- Usar
getContainercon unidpara dirigir la petición a una instancia concreta. - Decidir cuándo un contenedor está justificado y reconocer el antipatrón de delegarlo todo.
Dos capas, dos economías
La tentación es ver el Worker y el Container como dos formas de ejecutar código. Son algo más incómodo y más útil: dos economías distintas. El Worker corre en cientos de ubicaciones, arranca en cero milisegundos y se factura por petición y por milisegundos de CPU; su coste marginal es casi nulo, pero está acotado en potencia. El Container ofrece un sistema operativo completo con CPU, memoria y disco de sobra, pero es un recurso escaso —limitado por max_instances—, se despliega en menos ubicaciones y se factura por el tiempo que permanece vivo, con una penalización de segundos al despertar.
De esa asimetría sale una regla económica, no solo técnica: cada milisegundo de trabajo que retienes en el Worker es prácticamente gratis e instantáneo; cada milisegundo que empujas al Container cuesta tiempo de ejecución y arriesga el coste de despertarlo. La arquitectura, entonces, es un problema de optimización: maximizar el trabajo en la capa barata y global, minimizar el núcleo irreducible que baja a la capa cara.
El Worker enruta, el Container ejecuta
El Container nunca se expone al público: la única puerta es el Worker. Eso convierte al Worker en el cerebro y al Container en el músculo. El cerebro hace todo lo que un isolate sabe hacer —validar un token, aplicar límites de tasa, comprobar la caché, dar forma a la respuesta, enrutar— y solo delega la operación genuinamente pesada.
import { Container, getContainer } from "@cloudflare/containers";
export class PdfContainer extends Container {
defaultPort = 8080;
sleepAfter = "5m";
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 1. El front barato hace todo lo que un isolate puede hacer
const usuario = await autenticar(request, env);
if (!usuario) return new Response("No autorizado", { status: 401 });
const cacheada = await caches.default.match(request);
if (cacheada) return cacheada; // ni despertamos el contenedor
// 2. Solo el trabajo irreducible baja al backend pesado
const instancia = getContainer(env.PDF, usuario.tenantId);
const respuesta = await instancia.fetch(request);
// 3. El front vuelve a mandar: cachea la salida cara
if (respuesta.ok) {
await caches.default.put(request, respuesta.clone());
}
return respuesta;
},
};
Observa las dos salidas tempranas. Una petición sin autorizar y una que acierta en caché se resuelven sin tocar el Container: son gratis e instantáneas. Solo cuando el trabajo es real —generar el PDF— el Worker llama a getContainer con un id (aquí el del inquilino) y delega. Ese id da enrutado pegajoso: todas las peticiones de un mismo inquilino aterrizan en su propia instancia, lo que aísla clientes y aprovecha el estado caliente del contenedor.
flowchart TD R[Peticion] --> W[Worker front] W -->|no autenticado| X[401 sin tocar el backend] W -->|acierto en cache| HIT[Responder desde cache] W -->|trabajo real| C[getContainer y delegar] C --> P[Container pesado ejecuta] P --> W style W fill:#89b4fa,color:#11111b style P fill:#fab387,color:#11111b style HIT fill:#a6e3a1,color:#11111b
Cuándo tiene sentido (y cuándo no)
Un contenedor está justificado cuando se cumplen dos condiciones a la vez. La primera: el trabajo choca de verdad contra una de las fronteras del isolate —lenguaje, binario, recursos o sistema de ficheros—. La segunda: ese trabajo es una porción acotada que puedes poner detrás de un límite de petición limpio, de modo que el Worker le entregue una entrada y reciba una salida.
El antipatrón es el espejo de esto: enrutar toda la aplicación a través del contenedor “porque es más simple”. Al hacerlo tiras a la basura la capa global y gratuita, sumas la penalización de despertar a cada petición y reconstruyes exactamente el servidor siempre encendido del que venías huyendo, ahora con el Worker degradado a proxy tonto. La forma sana invierte los papeles: front listo y omnipresente, músculo potente pero escaso que se invoca solo por lo que nadie más puede hacer.
Hay una disciplina que gobierna toda arquitectura en el edge, y el patrón Worker-delante-de-Container es su encarnación más nítida. La disciplina es esta: existe un gradiente de capas —de la más barata, rápida y global a la más cara, lenta y escasa— y cada descenso de una capa a la siguiente debe ganarse, no concederse por comodidad. La caché del navegador es más barata que la caché del edge, que es más barata que el isolate, que es más barato que el contenedor, que es más barato que una máquina propia. Un buen arquitecto empuja cada gramo de trabajo tan arriba en ese gradiente como la física permita, y solo desciende cuando una restricción real se lo impone. El patrón que has visto invierte una intuición de tres décadas. En el mundo clásico, delante había un proxy tonto y delgado y detrás un servidor de aplicaciones gordo que hacía todo el pensamiento; el front era un mero repartidor. Aquí el front es el que piensa —autentica, cachea, limita, enruta, decide— y está en todas partes a la vez, mientras el backend es un músculo especializado que ni siquiera se entera de las peticiones que el front resolvió por su cuenta. La pregunta de diseño deja de ser “¿cómo escalo mi servidor de aplicaciones?” y se vuelve “¿cuál es la operación más pequeña que de verdad exige un sistema operativo, y puede todo lo demás resolverse antes de llegar a ella?”. Quien interioriza esto escribe sistemas donde el contenedor duerme la mayor parte del tiempo porque el front ya hizo el trabajo; y un contenedor que duerme es un contenedor que no cuesta. La elegancia y el ahorro, aquí, son la misma cosa.
- Toma una funcionalidad que exija un contenedor (generar un PDF, transcodificar un audio) y lista qué pasos puede hacer el Worker antes de delegar: autenticación, validación, caché, límites de tasa.
- Escribe el
fetchdel Worker con al menos dos salidas tempranas que eviten despertar el contenedor. Explica cuánto ahorras en cada una. - Justifica por qué el
idque pasas agetContainerdebería ser el identificador del inquilino y no un valor aleatorio. - Describe el antipatrón de “todo al contenedor” y enumera tres costes concretos que impone.
- Argumenta por qué “el contenedor duerme la mayor parte del tiempo” es señal de una buena arquitectura y no de una infrautilizada.