Designing queues you can reason about
A practical model for making asynchronous work observable without turning every incident into archaeology.
June 12, 2026 · 1 min read
Queues are easy to add and surprisingly hard to explain once they become part of a real system. The useful unit is rarely a single job. It is the story of the work: what requested it, which attempts ran, and where the result ended up.
Start with an identity
Give every unit of work a stable identifier and carry it through retries. A compact event type keeps the contract explicit:
type WorkEvent = {
workId: string
attempt: number
state: "queued" | "running" | "succeeded" | "failed"
occurredAt: string
}
The identifier lets an interface group related events without guessing from timestamps or payloads.
Measure pressure, not just volume
Queue depth alone is ambiguous. A hundred jobs may be healthy or alarming depending on arrival and processing rates. A simple approximation of pressure is
where is the arrival rate and is the completion rate. When for a sustained period, the backlog grows.
Preserve the trail
Do not overwrite the current status in place and discard the history. Append events, derive the current state, and retain enough context to answer the next debugging question.