Guaranteed message delivery: cost and when to use it
"Will I lose messages if a worker dies?" is the right question — and the answer is a trade-off, not a yes/no. Here's how guaranteed delivery works, what it costs, and how to choose per workload.
The first serious question anyone asks about a message-based system is: "if a worker grabs a message and then crashes, is that message lost?" It's the right question. The answer isn't a simple yes or no — it's a trade-off you should make deliberately, per workload, with your eyes open about the cost.
Two honest modes
@imqueue offers two delivery modes, and the difference is exactly about that crash scenario.
Unreliable (fast) delivery. A consumer takes a message and processes it. If it crashes before finishing, that message is gone. This is the fastest mode — there's no bookkeeping — and it's the right default for work that is frequent, idempotent-on-retry-elsewhere, or simply not costly to miss (think best-effort notifications, cache warmups, telemetry).
Guaranteed (safe) delivery. As a consumer takes a message, it's atomically moved into that consumer's own "processing" holding area, so a process that dies between taking a message and starting on it leaves the message behind to be rescheduled to another instance instead of swallowing it. The protection covers that hand-off rather than the whole handler: the entry is released once the message reaches your code, so a consumer killed part-way through the work loses that attempt like any other. This is still what you want for work that must not vanish — placing an order, charging a card, kicking off a payout — paired with a drain on shutdown for the planned case.
What guaranteed delivery costs
Safety isn't free, and it's useful to know the shape of the bill. In a recent @imqueue/core benchmark run — a 24-core Intel Core Ultra 9 275HX, Node.js 24, ~1 KB messages — unreliable delivery reached ~200,000 round-trip msg/sec and guaranteed delivery ~120,000 msg/sec. So safe mode costs roughly 40% of throughput (about 1.7× slower), keeping ~60%. Your numbers will differ with hardware and message size — see the benchmark post for the full run and how to reproduce it.
That's a very reasonable price for "never lose this message," and the key insight is that you don't pay it globally. You choose per queue. Latency-critical, loss-tolerant paths stay in the fast mode; critical paths run safe. You're not forced into one guarantee for the whole system.
The knob, and what it actually governs
Guaranteed mode stamps each hand-off with a time-to-live (safeDeliveryTtl, default 5 seconds; safeLockTtl through @imqueue/job). A sweep running on that same interval reclaims holding-area entries whose TTL has passed and puts them back on the queue. That's the mechanism behind rescheduling — and it is easy to read more into it than it does:
A slow-but-healthy task is not re-queued for being slow, and raising
safeDeliveryTtldoes not extend any protection over a long-running handler. The entry is released when the message reaches your code, so the TTL is a recovery deadline for an abandoned hand-off, not a processing deadline.
So tune it for recovery latency rather than against your p99: shorter brings an abandoned message back sooner, longer sweeps less often. What survives a restart that lands mid-handler is a drain on shutdown, not this TTL.
A quick decision guide
Ask two questions about each kind of message:
- If this is lost, does it matter? If no → unreliable is fine, and faster. If yes → guaranteed.
- Is the work idempotent? Guaranteed delivery is at-least-once: a message can be processed more than once (a hand-off whose release didn't land can be swept back onto the queue while the first consumer is still working, and
@imqueue/jobre-sends a job whose handler throws). So design critical handlers to be idempotent — safe to run twice — using an idempotency key or a dedupe check. This matters regardless of framework; at-least-once is the honest guarantee, not exactly-once.
Match the mode to the message and you get the best of both: raw speed where loss is acceptable, and durability where it isn't — without paying for durability everywhere. The delivery options are covered in the API reference; Getting Started gets you a service to try them on.
Read this page as plain markdown — no HTML, no navigation. For pasting into an LLM, or for an agent to fetch.
Building on @imqueue? The open-source packages live on GitHub and the docs at imqueue.org. Shipping inside a closed-source product? See commercial licensing & support.