Message Delivery Guarantees for Real-Time Apps
Real-time does not mean reliable. Here is how message delivery guarantees work, why at-least-once matters, and how to choose delivery semantics per message type.
Real-time and reliable are two different promises, and confusing them is how apps silently lose data. Fast delivery says a message arrives quickly. Guaranteed delivery says it arrives at all, even if the client was offline for a moment when you sent it. Most real-time systems deliver the first and quietly skip the second, which means a chat message sent during a two-second connection blip is gone forever and nobody knows. The fix is to treat delivery semantics as a decision you make per message type, on purpose.
The three delivery models
There are three delivery guarantees, and you should be able to name which one each message type needs.
At-most-once is fire and forget. The message is sent and if it lands, great, if not, oh well. It is the cheapest and it is correct for data that is immediately superseded. A live cursor position is the classic case. If one update is lost, the next one arrives in milliseconds and corrects it. Buffering a stale cursor would be worse than dropping it.
At-least-once means the message will be delivered, possibly more than once. The system holds it and retries until the client acknowledges receipt. This is what most meaningful messages need: chat, notifications, state changes, order updates. The cost is that the client must handle duplicates, because a retry after a lost acknowledgment delivers the same message twice.
Exactly-once is the holy grail and the most expensive. Delivered, once, no duplicates. True exactly-once is genuinely hard in a distributed system, and most of the time you approximate it with at-least-once delivery plus idempotent handling on the client, where processing the same message twice has the same effect as processing it once. That combination is usually the honest right answer.
Why the choice has to be per message
The mistake is picking one model for the whole system. A real app carries many kinds of messages with different tolerance for loss and duplication, so one global setting is always wrong for some of them.
Cursors and typing indicators want at-most-once, because freshness beats completeness. Chat and financial events want at-least-once with idempotency, because losing one is unacceptable and a rare duplicate is survivable. Forcing everything through one guarantee means either you pay exactly-once cost on cursor spam or you drop chat messages to keep things cheap. Both are bad. The right design lets you tag a message type with the guarantee it needs. Choosing deliberately instead of by accident is a theme across the real-time backend mistakes that break apps at scale.
What guaranteed delivery actually requires
At-least-once is not free, and understanding the machinery tells you why so many systems skip it. To guarantee delivery you need acknowledgments: the client confirms it received a message. You need a buffer or queue that holds unacknowledged messages until that confirmation comes. You need retry logic with limits so you do not retry forever into a dead client. And you need per-client tracking of what has and has not been delivered, so a client that reconnects gets exactly what it missed and not the entire history.
That last part connects delivery to presence and reconnection. When a client blinks offline and comes back, the system has to know where it left off and replay the gap. Delivery, presence, and session state are not three separate features. They are one system, which is why I argue against reassembling this stack per product. Build the queue and the acknowledgment tracking once, correctly, and every feature inherits reliable delivery.
The pattern I use
My rule is to default meaningful messages to at-least-once with idempotent handlers, and reserve at-most-once for high-frequency ephemeral data where the next update fixes any loss. Idempotency on the client is the cheap insurance that makes at-least-once safe, and it is far easier than chasing true exactly-once across nodes.
This is exactly the guarantee a complete real-time backend should own so you do not hand-roll it. AltoHost is built to handle delivery, acknowledgment, and reconnection replay as first-class concerns, so the application decides what a message means and the backend guarantees it arrives. That division is the point: your product logic should not be tangled up in retry queues, and your retry queues should not depend on your product logic.
Closing
Real-time is a latency promise. Reliability is a separate promise you have to make explicitly. Name the delivery guarantee each message type needs, default the important ones to at-least-once with idempotency, and let the ephemeral ones drop. Do that and your app stops losing data in the gaps between connections, which is where most real-time apps quietly bleed.