Call
APIs

WebSockets in Production: Handshakes, Frames, Authentication, Backpressure, and Scaling

Understand WebSockets beyond basic send/receive: the HTTP opening handshake, frames and masking, ping/pong and close semantics, subprotocols, Origin security, authentication, message authorization, bounded buffers, reconnect recovery, proxies, and multi-node scaling.

Published: January 14, 2026
Updated: September 12, 2026
12 min read
WebSockets in Production: Handshakes, Frames, Authentication, Backpressure, and Scaling

WebSockets in Production: Handshakes, Frames, Authentication, Backpressure, and Scaling

WebSockets give a browser and server a long-lived bidirectional channel, which is ideal for chat, collaboration, live control, and other interactive realtime features.

The browser API looks small:

const socket = new WebSocket("wss://api.example.com/realtime");

socket.addEventListener("message", function (event) {
  console.log(event.data);
});

socket.send("hello");

Production WebSockets are not small. You still need to design the opening handshake, authentication, per-message authorization, connection lifecycle, application protocol, slow-client behavior, reconnect recovery, proxy timeouts, and cross-node fan-out.

WebSocket begins as an HTTP handshake

RFC 6455 defines an opening handshake that starts as an HTTP request.

For the traditional HTTP/1.1 path, the client requests a protocol upgrade with headers such as:

GET /realtime HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
Origin: https://app.example.com

A successful server responds with 101 Switching Protocols and the computed Sec-WebSocket-Accept value.

After that, the underlying connection carries WebSocket frames instead of normal HTTP request/response messages.

This matters operationally because load balancers, reverse proxies, gateways, WAFs, and timeouts must understand the upgraded long-lived connection.

Use wss:// for production traffic

ws:// is unencrypted.

wss:// runs WebSocket over TLS and should be the normal production choice for authenticated or sensitive browser traffic.

TLS protects transport confidentiality and integrity. It does not authorize the user to join a room, read a tenant stream, or execute an application command.

Authentication and authorization remain application responsibilities.

WebSocket can also be bootstrapped over HTTP/2

The original RFC 6455 handshake uses HTTP/1.1 Upgrade.

RFC 8441 defines Extended CONNECT for bootstrapping WebSocket on an HTTP/2 stream when both peers support it.

That allows WebSocket traffic to coexist with other HTTP/2 streams in compatible infrastructure.

Do not assume every proxy, CDN, gateway, or client library uses the same HTTP-version path. Verify the exact production chain.

Once established, the application still uses normal WebSocket protocol semantics.

Frames are the protocol delivery unit

RFC 6455 defines frame opcodes for:

  • text
  • binary
  • continuation
  • close
  • ping
  • pong

Messages can be fragmented across frames. Most libraries hide fragmentation from application code, but frame semantics still matter when setting:

  • maximum payload size
  • compression
  • gateway limits
  • binary/text handling
  • security controls

Do not accept unbounded messages simply because normal clients send tiny JSON objects.

Client frames are masked, but masking is not encryption

RFC 6455 requires client-to-server frames to be masked. Servers do not mask frames sent to clients.

Masking is a protocol requirement intended to protect intermediaries from specific attack classes. It is not a confidentiality mechanism.

TLS is what protects sensitive traffic on the network.

A compliant server should reject invalid framing instead of trying to tolerate malformed protocol data.

Text and binary messages are both valid choices

Text frames are convenient for readable JSON:

{
  "type": "chat.message.send",
  "request_id": "req_123",
  "payload": {
    "conversation_id": "c7",
    "text": "hello"
  }
}

Binary frames can be useful for compact serialization or binary realtime state.

But binary is not automatically worth the complexity. For many business applications, a clear versioned JSON protocol is an excellent default until profiling shows serialization or bandwidth is a real bottleneck.

Define an application protocol above WebSocket

WebSocket gives you a transport, not business semantics.

A useful event envelope can include:

{
  "version": 1,
  "type": "order.updated",
  "event_id": "evt_900",
  "payload": {}
}

Client commands may include a request ID:

{
  "version": 1,
  "type": "chat.message.send",
  "request_id": "req_901",
  "payload": {}
}

Define explicitly:

  • message type names
  • schema/version strategy
  • request/event IDs
  • success/error responses
  • authorization scope
  • acknowledgement rules
  • maximum payload size
  • ordering semantics

Without this, several client versions quickly turn one socket into an undocumented collection of JSON shapes.

Use subprotocol negotiation only when it represents a real contract

The opening handshake supports Sec-WebSocket-Protocol.

A client can offer application subprotocols such as:

chat.v2, chat.v1

and the server selects one supported protocol.

This is useful when protocol negotiation genuinely matters. For ordinary application event evolution, one application-level version field may be easier than inventing a new subprotocol for every message change.

Validate Origin on browser handshakes

Browser WebSockets can carry existing cookies during the handshake. A malicious website may therefore attempt to open a WebSocket to your application using the victim's authenticated browser session.

RFC 6455 includes the Origin header as part of browser-origin protection, and OWASP recommends validating it against an explicit allowlist.

Accept exact trusted origins such as:

https://app.example.com

Avoid permissive wildcard or substring matching.

Origin validation protects one security boundary. It does not replace authentication.

WebSocket has no built-in user identity model

RFC 6455 defines transport semantics, not your login system.

Common browser authentication approaches include:

  • secure session cookies
  • short-lived connection credentials
  • framework-supported authentication during handshake

Be careful with bearer credentials placed in URL query strings because URLs may appear in proxy logs, monitoring, diagnostics, and error reports.

When URL credentials are unavoidable, keep them short-lived and scoped and ensure infrastructure does not log them.

Authenticate the connection, authorize every sensitive operation

An authenticated user is not automatically authorized for every room, tenant, or command.

For this command:

{
  "type": "conversation.subscribe",
  "conversation_id": "private-c7"
}

verify that the user currently belongs to private-c7.

Do the same for:

  • sending messages
  • joining topics
  • editing data
  • subscribing to tenant events
  • triggering administrative actions

Never trust a user, room, or tenant ID merely because it came through an authenticated socket.

Long-lived sockets need revocation behavior

A session can become invalid while the socket remains open.

Plan what happens when:

  • user logs out everywhere
  • password changes
  • account is disabled
  • tenant membership is removed
  • permission changes

For security-sensitive systems, close or re-authorize affected connections instead of waiting indefinitely for the network to disconnect them.

A mapping from session/user identity to active connections makes revocation possible.

Ping and Pong help detect dead connections

RFC 6455 defines Ping and Pong control frames.

Ping can act as a keepalive and liveness check. A peer receiving Ping responds with Pong according to protocol rules.

This matters because mobile networks, sleeping laptops, NAT mappings, and intermediary failures can leave connections that appear open but are no longer useful.

Use reasonable heartbeat intervals. Extremely aggressive heartbeats waste server work, bandwidth, and battery.

Application heartbeat is different from protocol Ping/Pong

Browser JavaScript does not expose RFC control-frame Ping/Pong in the same way many server libraries do.

If your product needs application-level liveness semantics, you may send a normal application message:

{ "type": "app.ping", "id": "p123" }

and expect an application response.

Keep the distinction clear when debugging connection health.

Close gracefully when possible

RFC 6455 defines a closing handshake using Close frames.

Use meaningful close behavior for cases such as:

  • normal shutdown
  • authentication failure
  • policy violation
  • server maintenance
  • unsupported application protocol

Do not put sensitive internal details into close reasons that may surface in client logs.

Clients should distinguish unexpected network failure from deliberate terminal closes where immediate reconnect would be useless.

Reconnect with bounded exponential backoff and jitter

A deployment or gateway failure can disconnect a huge client population at once.

If every client retries immediately, the recovering service gets a thundering herd.

A healthier strategy is:

attempt 1 → short randomized delay
attempt 2 → longer randomized delay
attempt 3 → longer again
later attempts → capped maximum delay

Reset the backoff after a meaningful healthy period.

Explicit policy/authentication closes and transient network failures should not necessarily use the same reconnect policy.

Reconnect is not state recovery

Opening a new socket only restores the transport.

The user may have missed durable state while disconnected.

A recovery flow can use a cursor:

reconnect
report last known event ID
fetch or replay missing durable state
resume live delivery

For state-oriented applications, refetching the canonical current object may be simpler than replaying every intermediate event.

Make disconnects normal and recoverable instead of trying to make one socket immortal.

The stable browser WebSocket API has no built-in backpressure

MDN explicitly warns that the standard WebSocket interface does not provide automatic backpressure.

send() queues bytes, and bufferedAmount reports how much data is waiting to be transmitted.

When bufferedAmount keeps rising, clients should stop producing unlimited work.

Possible policies include:

  • coalesce rapidly changing state
  • drop disposable events
  • pause producers
  • reconnect and recover durable state later

bufferedAmount is a signal; your application still defines the high-water policy.

Server-side queues must be bounded too

A fast producer sending to a slow client can create an ever-growing gateway queue.

Set limits by bytes, messages, and/or age.

When a connection falls too far behind:

  • drop obsolete ephemeral data
  • coalesce latest-state updates
  • disconnect and recover durable messages later
  • reject excess producer work where appropriate

One slow connection should never have permission to consume unbounded server memory.

Compression is not automatically a win

permessage-deflate can reduce payload size but costs CPU and memory and can create security concerns in applications that compress secrets together with attacker-influenced content.

Before enabling compression globally, measure:

  • typical message size
  • bandwidth saved
  • CPU impact
  • memory per connection
  • sensitive-data risk

Tiny JSON events may not justify the overhead.

Limit payload size and message rate

Treat every connected client as untrusted input.

Enforce limits on:

  • message bytes
  • decoded payload complexity
  • events per second
  • expensive commands
  • subscription count
  • room joins

Persistent connections can bypass normal HTTP request patterns and generate very high application work if the realtime layer has no equivalent rate limiting.

Validate every application message

Do not dispatch arbitrary decoded JSON directly into business handlers.

Validate:

  • allowed type
  • payload schema
  • string length
  • IDs
  • enum values
  • numeric ranges

Unknown or invalid messages should produce a predictable protocol error without crashing the connection process.

Horizontal scaling requires cross-gateway delivery

Suppose connections are distributed like this:

user A → gateway 1
user B → gateway 2
user C → gateway 3

An event created elsewhere must reach whichever gateway currently owns the recipient's connection.

Common designs use:

  • broker/pub-sub between gateway nodes
  • a connection-directory/routing layer
  • room/topic fan-out infrastructure

Keep irreplaceable user state outside gateway memory. If gateway 2 disappears, user B should reconnect elsewhere and recover durable state.

Sticky routing does not solve fan-out

A live socket naturally remains attached to one server for its lifetime.

Sticky reconnection may simplify some deployments, but it does not answer:

How does gateway 1 send an event to someone connected to gateway 7?

You still need cross-node routing or fan-out.

Proxies and load balancers need long-lived connection settings

Healthy sockets can be terminated by infrastructure because of:

  • idle timeout
  • maximum connection age
  • rolling deployment
  • proxy restart
  • upstream health changes

Verify:

  • WebSocket Upgrade/CONNECT support
  • read/idle timeouts
  • TLS termination
  • maximum connection limits
  • connection draining

Even perfect infrastructure will eventually close connections. Clients must recover.

Drain connections during deployments

A graceful gateway rollout can:

  1. stop accepting new connections on an old instance
  2. keep current sockets briefly while new instances become ready
  3. optionally signal clients to reconnect
  4. enforce a maximum drain window
  5. close remaining connections

Combined with reconnect jitter, draining reduces deployment-induced spikes.

Observe the full lifecycle

Useful metrics include:

  • active connections
  • new connections/sec
  • reconnect rate
  • handshake/auth failures
  • Origin rejections
  • close codes
  • abnormal disconnects
  • messages/sec by type
  • inbound/outbound bytes
  • per-connection queue pressure
  • slow-client disconnects
  • fan-out latency
  • gateway CPU/memory/event-loop or thread saturation

Normal HTTP access logs usually capture only the opening handshake. Add explicit WebSocket telemetry.

Avoid logging complete private message payloads or authentication tokens.

A practical production flow

Connect

  1. Open wss://.
  2. Validate browser Origin.
  3. Authenticate identity/session.
  4. Negotiate protocol/version if needed.
  5. Bind connection to user/session/device.

While connected

  1. Validate every message schema.
  2. Authorize every sensitive operation.
  3. Rate-limit abusive event types.
  4. Keep input/output queues bounded.
  5. Monitor liveness.
  6. Deliver only events the user is currently allowed to receive.

Disconnect and recover

  1. Record the close reason.
  2. Clean connection ownership/presence.
  3. Retry transient failures with backoff and jitter.
  4. Reauthenticate.
  5. Fetch/replay missed durable state from a cursor or version.
  6. Resume live delivery.

That is the difference between a WebSocket demo and a production WebSocket system.

When WebSocket is not the best choice

Use something simpler when it fits.

SSE may be better when the server mainly pushes events and client commands already use normal HTTP.

Polling may be better when updates are rare and seconds of latency are fine.

WebRTC is the natural browser technology when peer audio/video or direct peer data is central.

WebTransport is worth evaluating when independent streams or datagrams materially improve the protocol.

Choose WebSocket when sustained bidirectional client-server communication is actually required.

Production checklist

Before launching WebSockets, verify:

  • production traffic uses wss://
  • browser Origin is explicitly allowlisted
  • authentication is secure during handshake
  • authorization is enforced per command/subscription
  • session revocation can close live sockets
  • messages are versioned and schema-validated
  • payload/rate/subscription limits exist
  • dead connections are detected
  • reconnect uses backoff and jitter
  • missed durable state can be recovered
  • browser and server buffers are bounded
  • proxy/load-balancer timeouts are reviewed
  • deployments drain connections where practical
  • cross-node fan-out works
  • telemetry includes close codes, queue pressure, auth failures, and reconnects

WebSocket's biggest strength is also its biggest responsibility: once you replace short HTTP interactions with a long-lived bidirectional session, connection state, security, failure recovery, and flow control become part of your application protocol.

References

Tags:WebSocketsRFC 6455Real-TimeWeb SecurityBackend ArchitectureAPIs
ENTERPRISE SAAS ENGINEERING

BUILDING A SAAS PRODUCT?

Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.

EXPLORE SAAS SERVICES →