El `scheduled` handler: un Worker sin cliente
Un Worker no solo responde a peticiones: puede despertar solo. El handler scheduled es la función que Cloudflare invoca según un reloj, sin Request ni Response; su firma, sus tres argumentos, y cómo convive con fetch en el mismo isolate.
Un Worker que ya conoces es reactivo: duerme hasta que alguien llama a su puerta con una Request, y su vida entera consiste en devolver una Response. Pero existe una segunda puerta, y no la abre un cliente: la abre el reloj de Cloudflare. El scheduled handler es la función que la plataforma invoca cuando un cron trigger se dispara —sin petición que atender y sin respuesta que devolver—. Entenderlo como el gemelo de fetch es aceptar que un Worker también puede ser un sujeto que actúa por su cuenta, no solo un objeto al que se le pide algo.
- Entender que
scheduledes un handler invocado por el reloj de Cloudflare, no por una petición. - Leer la firma completa —
controller,envyctx— y qué expone cada argumento. - Distinguir
scheduleddefetchy ver cómo conviven en un mismo Worker. - Interpretar
controller.cronpara ramificar cuando hay varios horarios sobre el mismo handler.
Un evento que nadie pide
El runtime invoca fetch una vez por cada petición HTTP: hay un cliente al otro lado esperando bytes. El scheduled handler rompe esa premisa. Lo invoca el programador interno de Cloudflare cuando llega la hora marcada por un cron trigger, y no hay ningún cliente: no recibes una Request y no devuelves una Response. Su tipo de retorno es Promise<void>, porque no hay nadie a quien contestar.
export default {
async scheduled(controller, env, ctx) {
console.log("cron procesado");
},
};
Ese handler mínimo ya es un Worker de cron completo. Fíjate en lo que no hay: ni request, ni un new Response(...), ni un valor de vuelta. La plataforma no juzga el éxito por lo que devuelves —no devuelves nada—, sino por si tu handler y sus promesas se resuelven o lanzan una excepción.
La firma, campo por campo
Escrita con todos sus tipos, la firma revela tres argumentos, los mismos tres que ya viste en un fetch moderno pero con el primero cambiado:
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext,
): Promise<void> {
const cuando = new Date(controller.scheduledTime);
console.log(`disparado por ${controller.cron} el ${cuando.toISOString()}`);
},
} satisfies ExportedHandler<Env>;
Donde fetch recibía una Request, scheduled recibe un ScheduledController: el objeto que describe el evento de reloj. Sus campos son pocos y precisos.
controller
El evento de reloj: controller.cron (la expresión que disparó), controller.scheduledTime (milisegundos desde época) y controller.noRetry() para renunciar al reintento.
env
Tus bindings: KV, D1, R2, secretos. Idénticos a los del fetch; un cron accede a los recursos exactamente igual que un Worker HTTP.
ctx
El contexto de ejecución. Su método clave es ctx.waitUntil, que prolonga la vida del evento y merece su propia lección.
controller.scheduledTime no es una fecha, es un entero: milisegundos desde el 1 de enero de 1970 en UTC. Para trabajarlo como fecha lo envuelves con new Date(controller.scheduledTime). Y es la hora que la plataforma tenía programada, no necesariamente el instante exacto de ejecución: úsalo como referencia del horario, no como cronómetro de precisión.
fetch y scheduled en el mismo Worker
Un mismo Worker puede exportar los dos handlers a la vez: mismo archivo, mismo modelo de isolate, mismos bindings, dos puntos de entrada. También puede exportar solo scheduled —un Worker de fondo sin superficie HTTP—; en ese caso, una petición web no encuentra fetch que la atienda y termina en error.
export default {
async fetch(request, env, ctx): Promise<Response> {
return new Response("sirvo HTTP");
},
async scheduled(controller, env, ctx): Promise<void> {
// corro en el horario, sin nadie que pida nada
await env.KV.put("ultimo-cron", new Date().toISOString());
},
};
Cuando varios horarios apuntan al mismo Worker, todos invocan la misma función scheduled. Para ejecutar lógica distinta según el que se disparó, ramificas con controller.cron:
async scheduled(controller, env, ctx) {
switch (controller.cron) {
case "*/5 * * * *":
await sincronizar(env);
break;
case "0 0 * * *":
await limpiezaDiaria(env);
break;
}
}
flowchart TB Cliente -->|Request| Fetch[handler fetch] Fetch -->|Response| Cliente Reloj[programador de Cloudflare] -->|cron dispara| Scheduled[handler scheduled] Scheduled -->|sin respuesta| Fin[trabajo terminado]
Los dos handlers comparten la naturaleza del Worker —una función en el edge, efímera y sin estado entre invocaciones— pero difieren en su causa: a fetch lo causa un usuario, a scheduled lo causa el tiempo.
El instinto del desarrollador de backend es el del par petición/respuesta: el código duerme hasta que alguien llama, y su razón de ser es contestar. El cron invierte esa causalidad, y ahí está el salto mental. Con scheduled, el Worker deja de ser un objeto al que se le pide algo y pasa a ser un sujeto que actúa: nadie llama, nadie espera, no hay Response que devolver porque no hay destinatario. La ausencia de respuesta no es una carencia de la API, es la esencia del patrón —lo que se ejecuta no es una contestación, es una acción con voluntad propia disparada por el reloj—. Piensa en cómo se hacía esto antes del edge: un demonio cron en una máquina, un proceso siempre encendido, un planificador externo… todos exigen un servidor vivo esperando a que llegue la hora, y pagas por tenerlo despierto aunque no haga nada el noventa y nueve por ciento del tiempo. En el edge no hay máquina que mantener encendida: el programador es parte de la plataforma, materializa un isolate solo para correr tu handler cuando toca, y lo descarta al terminar. Cuando interiorizas que scheduled y fetch son las dos caras de un mismo modelo —la función en el edge, una reactiva y otra proactiva— dejas de pensar “necesito un servidor para la tarea programada” y empiezas a pensar “exporto un segundo handler”. Ese cambio de marco es lo que convierte el cron de una pieza de infraestructura a mantener en una simple función más de tu Worker.
- Escribe la firma del
scheduledhandler de memoria, con sus tres tipos:ScheduledController,EnvyExecutionContext. - Añade un
scheduledhandler a un Worker que ya tengafetch; registra conconsole.logelcontroller.crony elcontroller.scheduledTimeya convertido a fecha. - Razona en voz alta: si un Worker exporta solo
scheduledy abres su URL en el navegador, ¿qué ocurre y por qué? - Diseña dos horarios que apunten al mismo handler y escribe el
switchsobrecontroller.cronque separa su lógica.