Queues — Hand Off Now, Process Later
Every Hoplink redirect creates a click record: code, time, country. Writing each one to D1 inside the redirect adds a database round trip to every visit, and a burst of traffic from one viral post becomes a burst of writes. Cloudflare Queues let the redirect drop a small message in a queue and move on; a consumer later takes messages off in batches and writes them efficiently.
The rule
Producers send, consumers receive in batches, every message gets acked or retried.
Producer. Any Worker with a queue binding can send a message — any value that can be serialised, such as a JSON-like object:
// in the redirect handler
await env.CLICKS.send({ code, country: request.cf?.country, at: Date.now() });
return Response.redirect(target, 302);Consumer. A Worker exports a queue handler. The platform delivers messages in batches:
export default {
async queue(batch, env, ctx) {
for (const msg of batch.messages) {
try {
await saveClick(env, msg.body);
msg.ack(); // done, never deliver again
} catch {
msg.retry(); // try this one again later
}
}
},
};The config ties the two together:
{
"queues": {
"producers": [{ "binding": "CLICKS", "queue": "click-events" }],
"consumers": [{ "queue": "click-events", "max_batch_size": 50, "max_batch_timeout": 5 }]
}
}A batch is delivered when it reaches max_batch_size messages or when max_batch_timeout seconds pass, whichever comes first.
The gain is decoupling. The redirect no longer waits on D1, and if D1 is slow or briefly down, clicks pile up safely in the queue instead of failing. Producer and consumer can even be separate Workers, deployed and scaled on their own.
Key rules of delivery:
- Explicit acks.
msg.ack()andmsg.retry()settle one message;batch.ackAll()andbatch.retryAll()settle them all. - Throwing retries the batch. If the handler throws, every message not already acked is delivered again.
- At least once. A message can occasionally arrive more than once. Consumers should be idempotent: processing the same message twice must not double-count. Each message has a unique
msg.idyou can record to skip repeats.
The shortcut
Read every consumer with one question per message: "Where is this message's ack or retry?" Every path through the loop must end in exactly one of them — or deliberately let an error retry the batch.
Send
The redirect handler calls
env.CLICKS.send({...})and returns the redirect straight away.
Trap: acking at the top of the loop "to be safe". If the save then fails, the message is already gone.
Drills
Answer fast — trust the rule.
Which method does a Worker call to put a message on the CLICKS queue?
With max_batch_size: 50 and max_batch_timeout: 5, only 12 messages arrive in 5 seconds. What happens?
Why must a click consumer be idempotent?
The consumer acks message A, then throws while handling message B. What is redelivered?
Remember
- Producers send, consumers receive in batches, every message gets acked or retried.
- Shortcut: find each message's ack or retry on every path.
- Trap: acking before the work succeeds, and forgetting duplicates can arrive.
