Your WebSocket app works locally. Behind nginx in staging, everything looks fine — until exactly one minute: the connection closes, the client reconnects, the user sees a "disconnected" flash. Application logs show no exception. Only the proxy decided the session was idle.
This pattern — exactly 60 seconds — is one of the fastest diagnostics in web hosting. Before optimising onMessage, time the drop: the number of seconds often names the culprit.
WebSocket behind a reverse proxy
The browser sends an HTTP request with Upgrade: websocket. The proxy must pass Upgrade and Connection headers, not apply a classic HTTP timeout to a long-lived connection, and let bidirectional frames through without indefinite buffering.
| Component | Critical setting | Common mistake |
|---|---|---|
| nginx | proxy_read_timeout | 60s default |
| HAProxy | tunnel timeout | client timeout too short |
| AWS ALB | idle timeout | 60s minimum configurable |
| Cloudflare | WebSockets ON | edge timeout + ping needed |
WebSocket is not a "long" HTTP request — it is a distinct protocol after upgrade. The proxy must treat it as such.
Typical nginx configuration
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Adjust 3600s to your case — aligned with application heartbeat, not infinity. Also verify the backend accepts long connections and that intermediate firewalls do not impose their own idle limit.
Application heartbeat: mandatory in production
Even with high timeouts, send a WebSocket ping — or a light JSON message — every 30–45 seconds if any link in the chain cuts at 60 or 100 seconds. On Node (ws), Python (websockets) or Phoenix channels: enable native ping/pong or implement it explicitly.
Test with intentionally one minute of user inactivity — not only continuous traffic. A session open during a video call without application messages quickly reveals proxy or CDN limits.
CDN and managed hosting
If WebSocket crosses Cloudflare or a WAF, enable WebSockets in the dashboard, check duration or message limits per plan, and keep ping more frequent than edge timeout. With direct origin and no CDN, the issue is often nginx or HAProxy alone.
On PaaS (Clever Cloud, Railway, Heroku), read docs on long connections — some routers impose fixed caps. Compare suitable offers via our directory if real-time connections are core to your product.
The peak: the bug is not in your onMessage handler
Decide and move forward without blind spots
First time the exact disconnect interval and correlate it with documented timeouts on each hop (nginx, load balancer, CDN, PaaS). Then fix Upgrade headers and proxy timeouts, and add ping/pong more frequent than the shortest timeout. Test from production with one minute of deliberate inactivity — localhost often hides the middle layer. Document the final configuration in the deployment runbook so an nginx redeploy does not reset defaults.
Frequently asked questions
Why exactly 60 seconds?
Default nginx proxy_read_timeout and common load balancer value. A fixed interval points to transport layer, not a random application exception.
Which nginx directives for WebSocket?
HTTP/1.1, Upgrade, Connection upgrade, high proxy_read/send_timeout, map $http_upgrade for mixed HTTP and WebSocket traffic.
Does Cloudflare support long-lived WebSockets?
Yes on compatible plans; application ping remains recommended for silent or very long sessions.
Raise timeout without limit?
No — combine timeout aligned with heartbeat and regular ping more frequent than the strictest link in the chain.
If your WebSocket drops at one minute, do not debug the message — debug who closes the connection, and after exactly how many seconds.
