# Guaranteed message delivery: cost and when to use it

Source: https://imqueue.org/blog/guaranteed-message-delivery-cost/
Published: 2026-07-07
Author: Mykhailo Stadnyk — Creator & maintainer of @imqueue (https://github.com/Mikhus)

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](https://imqueue.org/blog/graceful-shutdown-zero-drop-deploys/) 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](https://imqueue.org/blog/benchmarking-imqueue-throughput/) 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 `safeDeliveryTtl` does 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](https://imqueue.org/blog/graceful-shutdown-zero-drop-deploys/), not this TTL.

## A quick decision guide

Ask two questions about each kind of message:

1. **If this is lost, does it matter?** If no → unreliable is fine, and faster. If yes → guaranteed.
2. **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/job` re-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](https://imqueue.org/api/); [Getting Started](https://imqueue.org/get-started/) gets you a service to try them on.

