Host async AI webhooks on Cloudflare Workers, Queues, and R2 for $0 idle cost
Stop paying $20–$50/mo for idle VPS containers just to receive payment or GitHub webhooks and run 30-second LLM jobs. Verify signatures at the edge, enqueue jobs in <10ms, and persist artifacts to R2.
Why default setups fall short
Webhook providers like Stripe, Waffo, GitHub, and Slack time out if your endpoint takes more than 3 to 10 seconds to respond—which means synchronous LLM calls inside a webhook handler inevitably fail and trigger duplicate retries. By splitting your backend into an instant Cloudflare Worker edge verifier (<10ms response) and a Cloudflare Queues background consumer writing outputs to zero-egress R2 storage, you get enterprise reliability with $0 idle compute cost.
Prerequisites before starting
- Cloudflare account with Workers, Queues, and R2 enabled
- Node.js 20+ and Wrangler CLI (npx wrangler)
- Webhook signing secret and LLM API key set via wrangler secret put
Step-by-step implementation
1. Verify HMAC/RSA webhook signatures using Web Crypto at the edge
Use the native crypto.subtle Web Crypto API inside your Worker fetch handler to verify the raw request body before parsing JSON, rejecting forged requests at the nearest edge pop in under 5ms.
const isValid = await crypto.subtle.verify( "HMAC", cryptoKey, hexToBytes(signatureHeader), new TextEncoder().encode(rawBody) );
2. Deduplicate event IDs and acknowledge in <15ms via Cloudflare Queues
Push the verified payload onto env.AI_JOB_QUEUE.send() and immediately return HTTP 200 OK to the webhook sender so it never times out while waiting for an AI model.
await env.AI_JOB_QUEUE.send({
eventId: payload.id,
prompt: payload.data.prompt,
receivedAt: Date.now()
});
return new Response(JSON.stringify({ ok: true, queued: true }), { status: 200 });
3. Process LLM completions in the Queue consumer with exponential backoff
Inside the queue() handler, run your 20–60 second LLM or workflow call. If a rate limit (429) occurs, call message.retry({ delaySeconds: 30 }) instead of losing the customer job.
export async function queue(batch: MessageBatch<JobPayload>, env: Env) {
for (const msg of batch.messages) {
try {
await runAiJobAndSaveToR2(msg.body, env);
msg.ack();
} catch (err) {
msg.retry({ delaySeconds: 30 });
}
}
}
4. Store generated deliverables in R2 with zero egress fees
Write the resulting JSON, PDF, or ZIP buffer directly to env.ARTIFACTS_BUCKET.put() with custom metadata so users can download their deliverable globally without AWS S3 bandwidth bills.