WebSockets, gRPC and streaming responses

Upgrade headers for long-lived sockets, timeouts and buffering for streaming, gRPC proxying with HTTP/2, and disabling buffering for server-sent events.

WebSocket proxying

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

upstream ws_backend {
    server 10.0.0.21:9000;
    keepalive 16;
}

server {
    listen 443 ssl;
    http2 on;
    server_name chat.example.com;

    location /ws/ {
        proxy_pass http://ws_backend;
        proxy_http_version 1.1;                    # required for Upgrade
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_read_timeout 3600s;                  # an idle socket must not be cut
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }
}
  • The map keeps a normal HTTP request sending Connection: close while an upgrade sends Connection: upgrade.
  • Without proxy_http_version 1.1 the Upgrade header is dropped and the handshake fails with a 400 or a hung connection.
  • The default proxy_read_timeout is 60 seconds; a quiet websocket is closed after that unless you raise it.
  • Send application-level pings so middleboxes do not expire the idle connection independently of nginx.

gRPC proxying

server {
    listen 443 ssl;
    http2 on;                          # gRPC requires HTTP/2 end to end
    server_name grpc.example.com;

    ssl_certificate     /etc/letsencrypt/live/grpc.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grpc.example.com/privkey.pem;

    location /helloworld.Greeter/ {
        grpc_pass grpc://grpc_backend;
        grpc_set_header X-Real-IP $remote_addr;
        grpc_read_timeout 300s;
        grpc_send_timeout 300s;
        grpc_socket_keepalive on;
    }

    # fall back to plain HTTP/1.1 for a non-gRPC health endpoint
    location = /health {
        grpc_pass grpc://grpc_backend;
        error_page 502 = /health_fallback;
    }
}

upstream grpc_backend {
    server 10.0.0.31:50051;
}
ProtocolDirectiveNotes
gRPC over TLSgrpc_pass grpcs://Encrypted to the backend
gRPC plaintextgrpc_pass grpc://Only on a trusted network
HTTP/2 to clientlisten 443 ssl; http2 on;Required for gRPC
Server-sent eventsproxy_buffering off;Prevents message batching

Server-sent events and streaming responses

location /events/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;               # deliver each chunk as it is written
    proxy_cache off;
    proxy_read_timeout 24h;
    chunked_transfer_encoding on;
    add_header X-Accel-Buffering no;   # tell the app not to buffer either
    add_header Cache-Control "no-cache";
}
⚠️
Compression breaks streaming. gzip buffers output in blocks, so a compressed event stream arrives in bursts or not at all. Exclude streaming paths from gzip, and disable buffering in the application as well as in nginx.

FAQ

Why does my websocket disconnect after exactly a minute?
The default proxy_read_timeout is 60 seconds. Raise it for the socket location and add application-level heartbeats so the connection also survives an idle NAT.
Can I serve gRPC and REST on the same port?
Yes. Route by path prefix — a gRPC service path such as /helloworld.Greeter/ is distinctive — and use grpc_pass for those locations with proxy_pass for the rest.

Rewrites, redirects and try_files nginx in containers and as a Kubernetes ingress

Last refreshed 2026-09-18.