WebSocket guarantees message order, but only within a single, open connection. When that guarantee breaks in production and not in local testing, the cause is almost always one of four things:
- An async message handler reordering messages in your own code
- A load-balanced reconnect landing a client on a different backend instance
- Replay overlapping with live delivery after a disconnect
- More than one publisher writing to the same stream
The first is a genuine one-time fix. The other three are infrastructure you commit to operating, not patches you apply once. Deciding which situation you're in, and what to do about it, is what this piece covers.
Key takeaways
- WebSocket's ordering guarantee comes from TCP, and it holds only for one open connection, not across reconnects, instances, or publishers.
- An await inside your own message handler can scramble message order on its own, with no network involved.
- Load-balanced reconnects, replay overlap, and multiple publishers are not one-time fixes. Each is infrastructure you keep operating.
- Out-of-order LLM tokens in AI streaming products are usually a multiple-publisher problem, not a model problem: a token-streaming worker and a separate agent or tool-call publisher are writing to the same channel with nothing coordinating the order between them.
- Whether to keep maintaining that reconnect, replay, and publisher-coordination work yourself, or hand it to a platform, comes down to ongoing engineering cost.
What WebSocket's ordering guarantee actually covers
WebSocket sends its messages over a single TCP connection, and TCP is what actually guarantees the order they arrive in. Every TCP segment carries a sequence number. The receiving side uses those numbers to reassemble segments in the order they were sent. It also asks for anything that goes missing to be sent again. WebSocket itself, standardized as RFC 6455, adds message framing on top of that connection: it packages your data into discrete messages. It does not add its own ordering logic, because TCP's ordering guarantee already applies to everything sent over that connection. The maintainers of the widely used ws library confirm this directly: WebSocket's ordering guarantee is TCP's guarantee, scoped to whichever single connection is currently open.
Close that connection and open a new one, or send a message over a different connection entirely. Either way, the ordering guarantee starts over on the new connection. It doesn't carry forward from the old one.
In production, connections drop and reopen far more often than they do on a stable local network. Each time that happens, the ordering guarantee starts over.
Why production breaks the ordering assumptions that hold locally
WebSocket messages that are out of order in production (but fine in your demo) are caused by one of two issues.
It could be a bug in how your code handles incoming messages. We’ll touch on this first, since it's fixable. Or, if your code isn’t the issue, then your infrastructure is. In this case, you need to build (and maintain) infrastructure to solve at least one of three problems:
- Reconnects landing on the wrong instance
- Replayed history overlapping with live messages
- More than one publisher writing to the same stream
The bug you can fix for good: async message handling
If your onmessage handler runs an await before it finishes handling a message, you can reorder messages entirely within your own application. No network involved.
ws.onmessage = async (message) => {
const blob = message.data;
const arrBuffer = await blob.arrayBuffer(); // execution pauses here
handle(arrBuffer);
};
The await pauses this handler and hands control back to JavaScript's event loop, which can run a different message's handler before this one finishes. A smaller message that arrives second can finish processing before a larger message that arrived first. So the bytes arrived over the wire in the right order, but the handler didn't process them in that order.
You can introduce this bug without realizing it. It looks correct in any test where message sizes and volumes don't vary enough to expose the timing gap. And this is the case in most demos.
The fix is to handle messages one at a time instead of letting each await run independently. A simple way to do this is to push incoming messages onto a queue and process them in the order they arrived.
The three ordering gaps that only appear once you scale past one instance
If you have ruled out your code being the issue, then it's possible you have a gap in your infrastructure. And because each of the following gaps only shows up once your system runs more than one server process, none of them are surfaced in your demo - when you're running a single instance on your laptop.
- Load-balanced reconnects
When a client reconnects after a network drop, a load balancer routes it to whichever backend instance looks healthiest. That's not necessarily the instance that was tracking that client's state. Each instance keeps its own connections in order. But nothing coordinates order across instances unless they share a pub/sub layer such as Redis or a message queue.
- Replay overlapping with live delivery
A reconnecting client needs history replayed to catch up on what it missed. Replayed messages and live messages can arrive at the same time. Both need something the client can use to put them back in the right order relative to each other, such as a monotonic sequence number. Without it, the two streams can end up merged in the wrong order.
- Multiple publishers writing to the same stream
Ordering guarantees are scoped to a single publisher on a single channel. They don't extend across every publisher writing to that channel. Two backend processes publishing to the same logical stream each keep their own messages in order, but nothing guarantees how the two interleave.
This shows up concretely in AI streaming products. Say one streaming worker publishes the model's response as tokens, and a second agent process appends tool-call events to the same conversation channel. Each keeps its own messages in order, but nothing guarantees how the two interleave for the subscriber. The result looks identical to the LLM “producing” tokens out of order, even though the model's output was never reordered. That's the same gap as two backend instances publishing to the same stream, just triggered by concurrent workers instead of concurrent backend instances.
What it takes to maintain reliable message ordering
Closing those three gaps means building three separate pieces of infrastructure: sticky routing, a shared coordination layer, and replay-sequencing logic.
Each takes real engineering time to build, and then continues to take engineering time to maintain.
Sticky routing looks straightforward to set up. Configure session affinity or a consistent hashing scheme at the load balancer, and most reconnects land where they should. It stops being straightforward the first time you add, remove, or resize instances. Each of those events can strand a client on the wrong node, and someone has to handle that case explicitly.
A shared coordination layer, typically Redis, is what lets independent instances agree on order at all. Wiring every instance to publish and subscribe through it takes a day. Operating it as a production dependency, with its own failure modes and its own scaling limits, takes considerably longer.
Replay-and-live sequencing is the piece most teams underestimate. Adding a sequence number and a replay window on reconnect is quick. Keeping that logic correct is ongoing work, not a step you complete once. It has to hold up every time reconnect behavior changes, a new publisher is added, or history retention changes.
As you'd expect, plenty of teams take this on themselves rather than buy their way out of it. But it comes at a real cost, in both time and money. Ably's State of Serverless WebSocket Infrastructure report surveyed more than 500 engineering leaders who built realtime infrastructure like this themselves.
It found that the average build takes 10.2 person-months. Half of self-built systems cost between $100,000 and $200,000 a year to maintain once they're running, and 65% had an outage or significant downtime in the 12 to 18 months before they were surveyed.
Deciding whether to keep maintaining WebSocket ordering yourself
If your WebSockets are already running in production, the real question isn't whether to build ordering guarantees from scratch. It's whether you want to keep extending and maintaining what you've already built, or hand that ongoing work to a platform built for it instead.
The first fix is usually cheap: a sequence number here, a queue there. Each fix after that compounds, because it has to work alongside the ones before it. You end up maintaining interactions between your sequence numbers and your reconnect logic, and between your reconnect logic and your load balancer's routing decisions.
How you weigh that depends on your team, but the check itself is simple.
- Track how many engineering hours go into this reconnect, replay, and coordination work in a typical month.
- Multiply that by loaded engineering cost.
- Compare the result to what a platform that already provides this would cost instead.
Colin Kennedy, Principal Product Engineer at Fin, frames that same check as a direct question: “Is maintaining this system helping you build a better product? If the answer is no, if you're spending time keeping the lights on instead of innovating, it's time to evaluate alternatives.”
Teams with ordering requirements that a general-purpose platform genuinely can't express have a real reason to keep building that infrastructure themselves. That's the exception. For most teams, that's engineering time spent away from the product it's meant to support.
Choosing a managed alternative with WebSocket ordering guaranteed
If you decide to hand this work to a platform instead of continuing to build it, look for three things:
- An ordering guarantee that's explicit about its scope: per channel, per publisher, not just “reliable messaging.”
- Automatic connection recovery that replays missed messages in order, rather than leaving your client to request them.
- An uptime track record that matches how critical this feature is to your product.
Any platform that provides this removes the ongoing work of routing reconnects to the right instance, and sequencing replay against live delivery. Although none will solve any bugs that remain in your message-handling code, or make decisions about what counts as a single logical publisher in your system.
Ably meets all three criteria - an explicit ordering guarantee, automatic connection recovery, and a matching uptime record.
- Ordering guarantee: Message ordering is guaranteed from any single realtime or non-realtime publisher to all subscribers on a channel.
- Connection recovery: Ably automatically re-establishes a failed connection and delivers the backlog of missed messages in realtime order, holding that connection state for up to two minutes after a disconnect.
- Uptime track record: 100% uptime over more than seven years, with no outages.
Publish idempotency adds a fourth guarantee on top: exactly-once delivery, removing the duplicate-message case a naive retry-based system can introduce.
Where this leaves you
WebSocket does guarantee order. The code that broke in production wasn't because the guarantee doesn't exist. It just ran into where the guarantee stops: the edge of a single connection. Production reopens that connection constantly, runs more than one backend instance, and often has multiple publishers (especially in AI use cases).
Fixing the async-handling bug is a genuine one-time fix, since it lives entirely in code you control. Fixing the other three (coordinated routing, a shared pub/sub layer, and replay sequencing) is not. Each is a piece of infrastructure that you continue to operate, not a patch you apply once. Ably's per-channel ordering guarantee and automatic connection recovery remove two of those three ongoing responsibilities.
Ably's automatic connection recovery removes the ongoing work of coordinated routing and replay sequencing. But its per-channel ordering guarantee still leaves you to decide what counts as a single logical publisher in your system - as you would have to on any platform.
Docs go deeper on how Ably's message ordering and connection recovery guarantees work in practice.
FAQ
Does TCP guarantee WebSocket message order?
Yes, for the duration of a single, open connection. TCP's guarantee does not cover delivery latency, and it does not deduplicate a retried message. It resets completely the moment that connection closes and a new one opens.
Why do WebSocket messages work fine locally but arrive out of order in production?
Local testing typically runs a single backend instance with a stable connection, so a reconnect never lands you anywhere else. Production runs multiple instances behind a load balancer, and connections drop and reopen far more often, especially on mobile networks. Each reconnect can land a client on a different instance than the one tracking its state, which is exactly the condition that breaks ordering.
How do you fix out-of-order WebSocket messages caused by async code?
Handle incoming messages one at a time. Don't run each one inside an await-based handler that can finish out of sequence before an earlier message does. A simple queue with a processing flag, or an async generator, processes messages in the order the connection already delivered them.
Do you still need your own sequence numbers if you use a realtime platform?
Generally no, for your own application logic. A platform that guarantees ordering per channel from a single publisher and coordinates replay on reconnect removes the need for your own sequence numbers. You still have to decide what counts as a single logical publisher in your system. That's a design decision no platform can make for you.
Why do WebSocket-delivered LLM tokens arrive out of order?
Almost always because more than one process is publishing to the same channel - a token-streaming worker and a separate event or tool-call publisher, for example. Each maintains its own order, but WebSocket's ordering guarantee doesn't extend across separate publishers on the same channel. The fix is the same as for any multi-publisher stream: treat token and event publishing as a single logical publisher, or use a platform whose ordering guarantee explicitly names its scope per publisher.



