Running cloudflare workers cron llm jobs eliminates the need to keep a server alive just to fire a prompt at 9am. This guide gives you a complete, copy-pasteable path from an empty directory to a deployed scheduled Worker that calls an LLM API and ships the result downstream.
Step 1: Scaffold the Worker project
Start with the official Cloudflare scaffolding tool. It generates a TypeScript Worker with a local dev server and Wrangler configured.
npm create cloudflare@latest llm-cron-worker
cd llm-cron-worker
npm install
Pick the “Hello World” scheduled Worker template if prompted, or just use the default worker and edit it. You only need src/index.ts and wrangler.toml.
Step 2: Declare cron triggers
Cron triggers are attached to the Worker via the [[triggers]] table in wrangler.toml. Cloudflare evaluates them in UTC. A single expression can be a standard five-field cron string or a named alias like "@daily".
name = "llm-cron-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
[[triggers]]
crons = ["0 9 * * *", "0 17 * * *"]
This runs the job at 09:00 and 17:00 UTC every day. You can list multiple strings. The free tier permits one cron expression; paid plans allow more.
Step 3: Store LLM credentials securely
Never embed API keys in source. Push them as encrypted secrets; Wrangler makes them available on the env object at runtime.
wrangler secret put LLM_API_KEY
# prompt will ask for the secret value
For the base URL, a non-sensitive var is fine in wrangler.toml:
[vars]
LLM_API_BASE = "https://api.openai.com/v1"
If you’d rather not wire provider failover yourself, point LLM_API_BASE at n4n.ai’s OpenAI-compatible endpoint that covers 240+ models and auto-falls back when a provider is rate-limited. The request shape below stays identical.
Step 4: Implement the scheduled handler
The Worker exports a scheduled method. Use ctx.waitUntil so the runtime keeps the function alive while the async job runs after the response is returned (cron invocations don’t have a client waiting, but this still matters for tail logs and error reporting).
interface Env {
LLM_API_KEY: string;
LLM_API_BASE: string;
}
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(handleJob(env));
},
};
async function handleJob(env: Env) {
const res = await fetch(`${env.LLM_API_BASE}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${env.LLM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "You are a concise summarizer." },
{ role: "user", content: "Summarize the top 5 Hacker News stories." },
],
max_tokens: 300,
}),
});
if (!res.ok) {
throw new Error(`LLM request failed: ${res.status} ${await res.text()}`);
}
const data = await res.json();
const downstream = await fetch("https://my-app.example.com/ingest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ summary: data.choices[0].message.content }),
});
if (!downstream.ok) {
throw new Error(`Downstream ingest failed: ${downstream.status}`);
}
}
Calling an OpenAI-compatible endpoint
The /chat/completions route is stable across OpenAI and most gateways. The model field accepts any string the backend supports. If you switch LLM_API_BASE to a gateway that honors client routing directives, you can pass route hints in headers or body depending on the gateway’s spec—check its docs.
Error handling and idempotency
Cron runs are at-least-once in practice. If your downstream write is not idempotent, store a hash of the date + job name in KV and skip if present:
const key = `job:${event.cron}:${new Date().toISOString().slice(0, 10)}`;
if (await env.KV.get(key)) return;
await env.KV.put(key, "done", { expirationTtl: 86400 });
Wrap the whole handleJob in try/catch and log with console.error. Wrangler tail will surface those.
Step 5: Test locally with simulated cron
Wrangler can fire the scheduled event without waiting for the clock.
wrangler dev --test-scheduled
In another shell:
curl "http://localhost:8787/__scheduled?cron=0+9+*+*+*"
The Worker’s scheduled handler runs immediately. Watch the dev terminal for fetch errors or malformed JSON. If you use KV locally, add a [[kv_namespaces]] binding and a wrangler.toml entry, then wrangler dev picks it up.
Step 6: Deploy and verify success
Deploy with one command:
wrangler deploy
Cloudflare registers the cron triggers from wrangler.toml automatically. To confirm the job actually ran, stream logs:
wrangler tail
You should see the scheduled invocation and any console.log output. Verification checklist:
wrangler tailshows noLLM request failederrors at the cron time.- Your downstream service (
/ingest) reports a new record with the expected summary text. - If you added KV idempotency, the key exists after the first run and the job is a no-op on manual re-trigger.
For a faster loop, re-run the local simulated cron after deploy using the production-like environment via wrangler dev --remote if you need real secrets.
Operational notes
Cron triggers on Cloudflare run within a few minutes of the scheduled time, not precisely on the second. Don’t design jobs that assume tight ordering across regions.
Keep the handler lean. A scheduled Worker has a CPU time limit (currently 10ms on free, more on paid with cpu_ms billing), but wall-clock can be longer if you waitUntil. Long LLM calls are fine because they are network-bound, not CPU-bound.
If you need per-token cost visibility, route through a gateway that meters usage. The earlier note about n4n.ai applies: it forwards provider cache-control hints and meters per token, so your Worker code stays the same while billing becomes observable.
Finally, version your wrangler.toml cron expressions. A typo like "0 9 * *" (four fields) fails silently at deploy. Validate with wrangler deploy --dry-run before pushing to production.