Fix Django WebSockets Disconnecting After 60 Seconds

Fix Django Channels WebSocket connections that drop after about 60 seconds of inactivity: raise Nginx proxy_read_timeout, configure load balancer idle timeouts, and send keepalive pings from the client or server.

Problem statement

WebSocket connections to your Django Channels app connect fine, then close with code 1006 after about a minute of no messages. Chat windows show "reconnecting", live dashboards miss updates, and the server logs WebSocket DISCONNECT at suspiciously regular intervals. Active connections that exchange messages frequently are fine.

Something between the browser and Daphne or Uvicorn closes idle connections. Nginx's default proxy_read_timeout is 60 seconds; AWS Application Load Balancers default to 60 seconds of idle time; Cloudflare closes idle WebSockets after 100 seconds. Each of them treats a quiet WebSocket like a stalled HTTP request.

Who closes the idle socket?Browserconnected, quietLoad balanceridle timeout 60 sNginxproxy_read_timeoutDaphne / Uvicornstill openThe shortest idle timeout on the path decides how long a quiet WebSocket survives.
  1. Set proxy_read_timeout and proxy_send_timeout to an hour or more in the Nginx WebSocket location.
  2. Raise the load balancer idle timeout (ALB: idle_timeout.timeout_seconds).
  3. Send a ping every 20-30 seconds from the client or server so connections never go idle.
  4. Handle reconnects in the client anyway; networks drop connections.
  5. Check server-side timeouts in Daphne or Uvicorn (--ws-ping-interval, --ws-ping-timeout).

Step-by-step solution

1. Measure the exact interval

Open the browser dev tools Network tab, filter to WS, and note how long connections last before closing. 60 seconds points to Nginx or an ALB at defaults; 100 seconds points to Cloudflare; 350 seconds to an AWS NLB; other values to custom settings or a corporate proxy.

2. Raise Nginx timeouts for WebSockets

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

location /ws/ {
    proxy_pass http://127.0.0.1:8001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

proxy_read_timeout is the maximum time between two reads from the upstream, not the total connection time. With pings every 30 seconds, even the default would be enough; raising it removes the dependency on pings.

3. Raise load balancer idle timeouts

  • AWS ALB: set the idle timeout attribute, up to 4,000 seconds (aws elbv2 modify-load-balancer-attributes --attributes Key=idle_timeout.timeout_seconds,Value=3600).
  • Google Cloud load balancer: backend service timeout applies to WebSockets as a total connection lifetime; set it high.
  • Cloudflare: the 100-second idle limit is fixed on most plans; you need pings.
  • Kubernetes ingress-nginx: annotations nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" and proxy-send-timeout.
default idle timeouts on the path (seconds)Nginx proxy_read_timeout60AWS ALB idle timeout60Cloudflare proxy100AWS NLB TCP idle350Ping interval to use25 s: below them all0200400A ping shorter than the shortest timeout keeps every hop happydjango-deployment.com
Check current provider documentation; defaults change and some are configurable.

4. Send keepalive pings

Server-level WebSocket pings from Uvicorn are protocol-level and invisible to your code:

uvicorn config.asgi:application --ws-ping-interval 20 --ws-ping-timeout 20

Daphne has --ping-interval and --ping-timeout. Some intermediaries do not count protocol pings as activity, so an application-level heartbeat is the most reliable:

const ws = new WebSocket(url);
const beat = setInterval(() => ws.readyState === 1 && ws.send(JSON.stringify({type: "ping"})), 25000);
ws.onclose = () => clearInterval(beat);
class LiveConsumer(AsyncJsonWebsocketConsumer):
    async def receive_json(self, content):
        if content.get("type") == "ping":
            await self.send_json({"type": "pong"})
            return
        ...

5. Reconnect gracefully

Deploys, network changes, and mobile sleep will always close connections. Reconnect with backoff and resynchronise state on reconnect:

function connect(attempt = 0) {
  const ws = new WebSocket(url);
  ws.onopen = () => { attempt = 0; resync(); };
  ws.onclose = () => setTimeout(() => connect(attempt + 1), Math.min(30000, 1000 * 2 ** attempt));
}
keeping WebSockets openNginx read and send timeouts raisedin the /ws/ locationLoad balancer idle timeout raisedALB, ingress annotationsHeartbeat every 20-30 secondsapp-level ping and pongClient reconnects with backoffand resyncs stateRelying on defaults end to end60 s idle somewhereTimeouts handle quiet periods; reconnects handle everything elsedjango-deployment.com
Even perfect timeouts do not survive deploys; build reconnection in from the start.

Verification check: open a WebSocket, send nothing for ten minutes, then send a message; it should arrive without a reconnect. Watch the server log for regular DISCONNECT lines disappearing.

Explanation

Reverse proxies and load balancers were built around HTTP requests, where a long silence usually means a stuck backend, so they close idle connections to free resources. A WebSocket is a single long-lived HTTP request that has been upgraded, and the proxy applies the same idle rules to it. Every hop's idle timer is reset by traffic in either direction, so a small periodic message keeps the whole chain open, while raising each timeout removes the need for frequent traffic.

Edge cases / notes

  • Mobile browsers suspend background tabs and kill sockets regardless of timeouts.
  • Corporate proxies may close WebSockets aggressively or block them; wss:// on port 443 passes more often than ws://.
  • Deploys restart Daphne or Uvicorn and close every connection; stagger restarts or drain connections.
  • Channel layer problems (Redis unreachable) produce disconnects on send, not on idle; check consumer logs.

Next steps

FAQ

Why do WebSockets close after exactly 60 seconds?

Nginx's default proxy_read_timeout and AWS ALB's default idle timeout are both 60 seconds.

Are protocol pings enough?

Usually, but some intermediaries ignore them. An application-level heartbeat message is the most reliable.

Can I set proxy_read_timeout to unlimited?

Nginx needs a finite value. One hour or more, combined with heartbeats, is effectively unlimited.

Does Cloudflare support long WebSocket connections?

Yes, but idle connections are closed after about 100 seconds, so send heartbeats.

Keep going