WebSocket Upgrade Handling: Apache vs. Nginx
Adam C. |

Today I learned something important about how Apache and Nginx handle WebSocket upgrades.

In Apache, it’s straightforward: you can use RewriteCond %{HTTP:Upgrade} =websocket to detect when a client is actually requesting a WebSocket. Only then do you forward with the ws:// protocol and set the Connection/Upgrade headers. If the request is just plain HTTP, you skip all that and proxy normally. Clean separation.

Photo by Neven Krcmarek on Unsplash

In Nginx, things work differently. If you follow common examples online, you’ll often see this in every location block:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

The problem? That sends Connection: upgrade even for normal HTTP requests. If your upstream isn’t expecting it, you can end up with weird parsing issues. It’s like telling your backend “this is a WebSocket” when it really isn’t.

The better approach in Nginx depends on your situation:

If you don’t use WebSockets at all:
Don’t set Upgrade or Connection headers anywhere. Just keep the proxy clean.

If you use WebSockets only on a certain path:
Put the Upgrade/Connection headers in a dedicated location /ws/ block, and leave normal location / untouched.

If you want to emulate Apache’s conditional logic everywhere:
Use a map in nginx.conf to set Connection to "upgrade" only when $http_upgrade is present, otherwise "close".

Example:

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

location / {
  proxy_http_version 1.1;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
  proxy_pass http://127.0.0.1:8082;
}

Takeaway

Apache makes it easy to branch on WebSocket vs. HTTP with RewriteCond. In Nginx, you either separate into different location blocks or use a map. And if you don’t need WebSockets at all, the cleanest solution is to drop the Upgrade/Connection headers entirely.

Want me to also make a short one-paragraph version of this (like you did for the header-size blog)?