Delivery guarantees
Message ordering
Socket.IO does guarantee message ordering, no matter which low-level transport is used (even during an upgrade from HTTP long-polling to WebSocket).
This is achieved thanks to:
- the guarantees provided by the underlying TCP connection
- the careful design of the upgrade mechanism
Example:
socket.emit("event1");
socket.emit("event2");
socket.emit("event3");
In the example above, the events will always be received in the same order by the other side (provided that they actually arrive, see below).
Message arrival
At most once
By default, Socket.IO provides an at most once guarantee of delivery:
- if the connection is broken while an event is being sent, then there is no guarantee that the other side has received it and there will be no retry upon reconnection
- a disconnected client will buffer events until reconnection (though the previous point still applies)
- there is no such buffer on the server, which means that any event that was missed by a disconnected client will not be transmitted to that client upon reconnection
As of now, additional delivery guarantees must be implemented in your application.
At least once
From client to server
From the client side, you can achieve an at least once guarantee with acknowledgements and timeouts:
function emit(socket, event, arg) {
socket.timeout(2000).emit(event, arg, (err) => {
if (err) {
// no ack from the server, let's retry
emit(socket, event, arg);
}
});
}
emit(socket, "foo", "bar");
In the example above, the client will retry to send the event after a given delay, so the server might receive the same event several times.
Even in that case, any pending event will be lost if the user refreshes its tab.
From server to client
For events sent by the server, additional delivery guarantees can be implemented by:
- assigning a unique ID to each event
- persisting the events in a database
- storing the offset of the last received event on the client side, and send it upon reconnection
Example:
Client
const socket = io({
auth: {
offset: undefined
}
});
socket.on("my-event", ({ id, data }) => {
// do something with the data, and then update the offset
socket.auth.offset = id;
});
Server
io.on("connection", async (socket) => {
const offset = socket.handshake.auth.offset;
if (offset) {
// this is a reconnection
for (const event of await fetchMissedEventsFromDatabase(offset)) {
socket.emit("my-event", event);
}
} else {
// this is a first connection
}
});
setInterval(async () => {
const event = {
id: generateUniqueId(),
data: new Date().toISOString()
}
await persistEventToDatabase(event);
io.emit("my-event", event);
}, 1000);
Implementing the missing methods (fetchMissedEventsFromDatabase()
, generateUniqueId()
and persistEventToDatabase()
) is database-specific and is left as an exercise for the reader.
References:
socket.auth
(client)socket.handshake
(server)