El modelo mental: una función pura de petición a respuesta
Un Worker es, en esencia, una función que transforma una Request en una Response. No hay servidor que mantener ni estado garantizado entre peticiones. Interiorizarlo reordena cómo diseñas software en el edge.
Has visto el fetch handler, la firma con env y ctx, el ciclo local y el enrutado a mano. Toca ahora subir un escalón y quedarte con el modelo mental que lo une todo: un Worker es, en esencia, una función que transforma una Request en una Response. No hay servidor que mantener ni estado que sobreviva de forma fiable entre peticiones. Interiorizar esto reordena cómo diseñas software en el edge.
- Ver el Worker como una función de petición a respuesta, no como un servidor.
- Entender que no hay estado garantizado entre peticiones.
- Saber dónde vive el estado entonces: en los bindings de
env. - Extraer las consecuencias de diseño de este modelo.
Una función, no un servidor
En el modelo tradicional, arrancas un servidor: un proceso de larga vida que escucha en un puerto, acepta conexiones, mantiene variables en memoria entre peticiones y sigue ahí, consumiendo recursos, aunque nadie le hable. Tú lo cuidas: lo escalas, lo reinicias, vigilas su memoria.
Un Worker invierte esa imagen. No mantienes un proceso; aportas una función. La plataforma la invoca cuando llega una petición y, conceptualmente, desaparece cuando ha respondido. No hay puerto que escuchar, no hay proceso que vigilar, no hay servidor al que hacer ssh. La unidad no es una máquina encendida, sino una invocación.
// No es un servidor que arrancas y mantienes.
// Es una funcion que la plataforma llama por cada peticion.
export default {
async fetch(request, env, ctx) {
return new Response("Una invocacion, una respuesta");
},
};
Ni siquiera es como una función serverless clásica de las que arrancan un contenedor bajo demanda y pagan el peaje del cold start. Un Worker corre en un isolate —un espacio aislado dentro de un proceso ya caliente—, así que no hay contenedor que levantar. Esa diferencia técnica es la que sostiene todo el modelo mental: si arrancar cuesta cero, no tiene sentido mantener nada encendido “por si acaso”.
Servidor tradicional
Un proceso encendido que escuchas, escalas, reinicias y vigilas. Guarda estado en memoria y está atado a una región.
Worker
Una función que la plataforma invoca por petición. Sin proceso que cuidar, sin región fija, sin estado que sobreviva por defecto.
Sin estado entre peticiones
De ahí se sigue la propiedad más importante: no debes contar con que nada sobreviva entre una petición y la siguiente. Cada invocación empieza, en la práctica, como una hoja en blanco. Una variable que incrementes en una petición no estará de forma fiable ahí en la próxima.
let contador = 0; // NO cuentes con esto entre peticiones
export default {
async fetch(request, env, ctx) {
contador++;
// Puede valer 1, puede valer 50, puede reiniciarse.
// No es una fuente de verdad.
return new Response(`contador poco fiable: ${contador}`);
},
};
Con precisión: el estado global de tu módulo puede persistir mientras el mismo isolate atienda varias peticiones, y a veces lo hace. Pero no lo controlas tú. Los isolates se crean y se reciclan según la carga y la geografía, dos peticiones seguidas pueden caer en isolates distintos —incluso en centros de datos distintos—, y la plataforma puede descartar el tuyo en cualquier momento. Por eso la regla no es “el estado global no existe”, sino algo más útil: nunca lo uses como fuente de verdad. Sirve como caché best-effort; jamás como almacén de datos.
Hay un regalo escondido en este modelo. Como cada invocación es independiente y no comparte memoria mutable con las demás, no tienes que proteger estructuras en memoria con locks ni pelearte con condiciones de carrera dentro del Worker. La concurrencia que en un servidor tradicional te obliga a razonar sobre hilos y secciones críticas aquí simplemente no aparece: mil peticiones a la vez son mil funciones que no se ven entre sí. La coordinación, cuando de verdad la necesitas, la resuelve un Durable Object, no tu código de compute.
Dónde vive el estado entonces
Si el Worker no guarda estado, ¿dónde va? La respuesta es la clave de toda la arquitectura del edge: el estado vive fuera del compute, en los bindings que recibes por env.
KV
Para datos de lectura frecuente y escritura rara, replicados globalmente. Configuración, sesiones.
D1
Una base de datos SQL cuando necesitas consultas relacionales y transacciones.
Durable Objects
Cuando necesitas estado coordinado y consistente con identidad única: un contador de verdad vive aquí.
R2
Para objetos y archivos grandes, sin cargos de egress.
El Worker se vuelve, así, la capa de lógica sin estado; el estado se delega a servicios diseñados para guardarlo. Ese contador que antes era poco fiable se vuelve correcto en el momento en que lo mueves a un binding:
export default {
async fetch(request, env, ctx) {
const previo = Number((await env.CONTADOR.get("total")) ?? "0");
const total = previo + 1;
await env.CONTADOR.put("total", String(total));
return new Response(`total fiable: ${total}`);
},
};
La lógica es la misma; lo que cambia es dónde vive el número. Esa mudanza —de la memoria del proceso a un servicio de estado— es, en miniatura, todo el arte de diseñar para el edge.
flowchart LR Req[Request] --> W[Worker sin estado] W --> Res[Response] W <-->|env| Estado[KV D1 Durable Objects R2]
La palabra “pura” tiene aquí un matiz honesto. Un Worker no es una función matemáticamente pura —tiene efectos: escribe en D1, llama a otras APIs—. Lo que sí es cierto, y lo que importa, es que no arrastra estado oculto entre invocaciones: todos sus efectos pasan por env y por ctx, de forma explícita. Esa es la disciplina que lo hace predecible.
Las consecuencias de diseño
Este modelo no es una limitación: es lo que hace posible el edge. Y trae consecuencias concretas:
- Escala horizontal sin esfuerzo. Si una función no guarda estado, la plataforma puede ejecutar un millón de copias a la vez sin coordinarlas. Escalar es, literalmente, invocar más veces.
- Sin cold starts. No hay un proceso que arrancar; un isolate se activa en microsegundos. Por eso el modelo de coste es por petición y por CPU usada, no por servidor encendido.
- Cerca de cada usuario. Como no hay una máquina con estado atada a una región, la misma función corre en cientos de ubicaciones a la vez.
- Testeable como una función. Sin estado oculto, un Worker es fácil de razonar y de probar: mismas entradas, misma salida.
Ninguna de estas cuatro propiedades es un truco de marketing: las cuatro brotan de la misma raíz, la ausencia de estado en el compute. Quítale el estado a una función y se vuelve clonable, barata de arrancar, ubicua y predecible. Devuélveselo y pierdes las cuatro a la vez. Por eso el modelo mental no es un consejo de estilo, sino la palanca de la que cuelga toda la economía del edge.
Puesto en preguntas, el cambio de diseño se resume así:
- ¿Qué entra? — una
Request, y nada más de lo que dependas. - ¿Qué sale? — una
Response, construida a partir de esa entrada. - ¿Qué estado hace falta? — el que no quepa en la petición.
- ¿Quién lo guarda? — nunca el Worker; siempre un servicio de
env.
La tentación al llegar de un backend tradicional es ver la ausencia de estado como una carencia: cómo que no puedo guardar nada en memoria. Es justo al revés: esa ausencia es la condición que hace posible todo lo demás. Un proceso con estado está atado a una máquina, a una región, a un ciclo de vida que hay que cuidar; no puedes multiplicarlo sin resolver el problema de sincronizar sus memorias. Una función pura de Request a Response no tiene ese problema: al no depender de nada entre invocaciones, se puede clonar sin límite, ejecutar en cualquier punto del planeta y arrancar sin coste. El edge no es servidores en muchos sitios, es una función que corre en todas partes porque no arrastra estado. Y el estado no desaparece —los datos siguen existiendo—, sino que se separa del compute y se confía a servicios especializados, KV, D1, Durable Objects o R2, a los que el Worker llama a través de env. Ese es el cambio de eje que arrastra todo el track: dejas de preguntarte cómo mantengo mi servidor y empiezas a preguntarte si mi lógica es una transformación sin estado y dónde vive el estado que necesita. Quien interioriza esto no está aprendiendo un producto más: está adoptando la arquitectura del edge.
- Coge un endpoint que hayas escrito en un servidor tradicional. Identifica qué estado guardaba en memoria del proceso.
- Reescríbelo mentalmente como función pura: qué entra en la
Request, qué sale en laResponse, y qué estado hay que mover a un binding. - Explica por qué un
let contador = 0global no es fiable en un Worker, citando el matiz de los isolates. - Formula, con tus palabras, la pregunta de diseño del edge: separar la lógica sin estado del estado que vive en
env.