A logged-out user keeps receiving live updates
“After logout, a password reset, or an admin revoking access, the old tab still shows new orders and messages in real time, sometimes for days, because its WebSocket or SSE stream was never closed.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
1008WebSocket close codes · RFC 6455 · standardThe endpoint received a message that violates its policy. It is the generic code when no more specific one fits, often used for authentication or authorization failures.
Put a machine-readable reason (for example "session_revoked") in the close reason, and make the client stop retrying instead of looping on reconnect.
Official reference for 1008 (opens in new tab)Code definition reviewed
THE PRINCIPLE
An open connection is a cached authorization decision; give it an expiry and a way to be revoked.
FIRST MOVES
- Reproduce: connect, revoke the session or let the token expire, and see whether events keep arriving. Check SSE and long-polling too.
- Store the credential's expiry on the connection and close it with 1008 when it passes, unless the client has sent a fresh token in a re-auth message first.
- Index connections by user and session. On logout, revocation, or a role change, publish an event that every instance consumes to close the matching sockets.
- Authorize each subscription and each sensitive message against current permissions, not the ones checked at connect.
- Keep tokens out of query strings (they end up in access logs): use a cookie with an Origin check, a short-lived one-time ticket, or an auth message as the first frame.
TOOLS: THEN → NOW
Validate the JWT once at connectExpiry timer per connection plus in-band re-auth messages
Revocation that only clears the HTTP sessionRevocation broadcast through the backplane, e.g. Socket.IO io.in(room).disconnectSockets() across nodes
PATTERN SNAPSHOT
function onAuthenticated(ws: Conn, session: Session) {
connections.add(session.userId, session.id, ws);
// Clamp: Node and browsers fire delays above 2^31-1 ms (~24.8 days) at once.
const ms = Math.min(session.expiresAt - Date.now(), 2 ** 31 - 1);
ws.authTimer = setTimeout(() => ws.close(1008, "token_expired"), ms);
}
// Every instance subscribes; the auth service publishes on logout/revoke.
bus.subscribe("session.revoked", ({ userId, sessionId }) => {
for (const ws of connections.find(userId, sessionId)) {
clearTimeout(ws.authTimer);
ws.close(1008, "session_revoked"); // client signs out, no auto-reconnect
}
});CLOSE THE AI. EXPLAIN THIS.
Your access tokens expire after 15 minutes. Why can a WebSocket opened with one still deliver data an hour later, and what two mechanisms close that gap?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
Guide reviewed