What's underneath

Three pieces, and only one of them is ours. That's deliberate: the part of a distributed queue that is easiest to get wrong is the part we didn't write.

On this page

What's underneath

Delivery — asynq

Enqueueing, delayed delivery, exponential backoff, and reclaiming work from a worker that died mid-task all come from asynq, which plenty of Go projects already run in production. Delivery is at-least-once, so the same event can arrive twice.

State — two Redis hashes per call

<callId>_status holds one field per step; <callId>_meta holds the metadata you passed in, memoised values, and delivered signals. This is what makes at-least-once safe: a duplicate delivery finds the step already done and skips it instead of running it twice.

Ownership — CAS, epoch, heartbeat

Every status write goes through a Redis Lua script doing an atomic compare-and-set. A running step carries an epoch and a heartbeat, so a process that froze and came back can't overwrite work someone else has since finished. Each call also holds a replay lease — 30s TTL, renewed every 10s — so two workers never replay it at once; kill a process and the lease simply expires.

github.com/hibiken/asynq

Queues and namespaces

All flows share one pair of queues: gotick and gotick_critical. The critical one drains first, so a flow that already started gets to finish instead of every flow crawling forward together. The event carries the flow id, and the consumer dispatches on it.

The pair does not grow with the number of flows, and that matters more than it sounds: with a queue per flow the poller has to walk every queue, so fifty flows means a hundred queues being polled even when the system is completely idle.

Config.Queue changes the namespace, default gotick. Every worker on one namespace should register the same set of flows. A worker handed a flow it doesn't know re-publishes the event rather than dropping it, because dropping would strand someone else's flow forever. A rolling deploy passes through this briefly and settles within seconds.

But two unrelated services sharing the default namespace bounce every event repeatedly: with eight workers and a one-in-eight hit rate, a two-step flow takes seconds instead of milliseconds. Give them separate namespaces.

gotick.Config{ Queue: "myapp", // default "gotick"}

Render diagnostics