Rate Limiting — Let the Queue Set the Pace
Hoplink checks every new target URL against an outside link-safety service before the preview goes live. The service allows 20 requests per second and answers 429 Too Many Requests beyond that. On a quiet day that is plenty. Then a partner imports 30,000 links at once — and the safety service starts refusing almost every call. The queue is where you control the pace.
The rule
Size the batch, cap the consumers, back off on refusal.
A queue already absorbs bursts: producers can send 30,000 messages in a minute, and they simply wait. How fast they leave is set by three consumer controls:
{
"queues": {
"consumers": [{
"queue": "safety-checks",
"max_batch_size": 10,
"max_concurrency": 2,
"max_retries": 5
}]
}
}max_batch_size— how many messages one consumer invocation receives.max_concurrency— how many consumer invocations may run at the same time. By default the platform scales consumers up automatically when the backlog grows; setting a cap stops it from overwhelming whatever sits downstream.- Backing off — when the service answers 429, do not hammer it. Retry the message later with a delay:
async queue(batch, env) {
for (const msg of batch.messages) {
const res = await fetch("https://safety.example/check", {
method: "POST", body: JSON.stringify({ url: msg.body.url }),
});
if (res.status === 429) {
msg.retry({ delaySeconds: 30 * msg.attempts }); // wait longer each time
} else {
await saveVerdict(env, msg.body.code, await res.json());
msg.ack();
}
}
}msg.attempts counts deliveries, so the wait grows with each refusal — a simple form of backoff.
The worst-case rate is roughly concurrency × batch size ÷ time per batch. Two consumers, each handling ten checks one after another at about a second per check, send around two requests per second, comfortably below the limit. Raise the numbers step by step while watching for 429s.
(For limiting incoming traffic — say, one visitor creating hundreds of links a minute — Workers offer a separate Rate Limiting binding, or you can count requests in a Durable Object. That protects you; the queue settings protect services you call.)
The shortcut
Start from the downstream limit and work backwards: limit → concurrency → batch size → backoff. Never start from "as fast as possible".
Read the limit
The service allows 20 requests per second. Aim well under it, say 10.
Trap: retrying a 429 with no delay. The message comes straight back, the service refuses again, and the retries burn through max_retries in seconds.
Drills
Answer fast — trust the rule.
Which consumer setting caps how many invocations run at the same time?
The safety service replies 429. What is the best response in the consumer?
What happens to 30,000 messages sent faster than the consumers can handle them?
Where should you begin when setting consumer limits for an outside API?
Remember
- Size the batch, cap the consumers, back off on refusal.
- Shortcut: limit → concurrency → batch size → backoff.
- Trap: instant retries on 429 that burn through every attempt.
