What does upstream mean in nginx? upstream vs proxy_pass
Your Laravel site returns 502 Bad Gateway and the error log says connect() failed (111: Connection refused) while connecting to upstream. You never typed the word "upstream" anywhere in your config, which is usually the moment people search "what does upstream mean in nginx?"
In nginx, "upstream" means any backend server nginx passes requests to: PHP-FPM, a Node or Octane process, another web server. The upstream directive names a group of those servers so proxy_pass or fastcgi_pass can reference it, adding load balancing, keepalive connections and failover that a plain address can't give you.
If you run one backend on the same VPS, a plain proxy_pass or fastcgi_pass to its address is fine and the upstream block is optional. If you need keepalive, several servers or failover, use a named upstream block, and if the backend's IP changes while nginx is running (Docker, cloud load balancers), use a variable with resolver.
What does upstream mean in nginx when you never wrote an upstream block?
Nginx treats every backend as an upstream, even when you only wrote an address. This:
location / {
proxy_pass http://127.0.0.1:8000;
}
creates an implicit upstream behind the scenes. That's why the error log talks about "upstream" for a config that never mentions it. "Upstream" is the server nginx talks to, and "downstream" is the browser talking to nginx.
So you really have three ways to point nginx at a backend:
- An inline address in
proxy_passorfastcgi_pass. - A named
upstreamblock in thehttpcontext, referenced by name. - A variable plus
resolver, so nginx looks up the hostname at request time.
Inline proxy_pass vs upstream block vs resolver variable
| Criterion | Inline address | Named upstream block | Variable + resolver |
|---|---|---|---|
| Load balancing across servers | Only if one hostname resolves to several IPs | Yes (round-robin, least_conn, ip_hash, hash) | Only via DNS |
| Keepalive connections to backend | No | Yes (keepalive) | No |
Failover options (max_fails, backup) | No | Yes | No |
| Picks up backend IP changes without reload | No | Generally no | Yes |
| nginx starts if backend hostname doesn't resolve | No | No | Yes |
| Reuse across many locations | Repeat the address | One name everywhere | Repeat the variable |
The rows below are standard documented behaviour. Where I give a preference, I'll say so.
Load balancing: only the upstream block does it properly
If you have two app servers, a named block is the only sane option:
# /etc/nginx/conf.d/laravel-upstream.conf (inside http context)
upstream laravel_web {
least_conn;
server 10.0.0.11:80 max_fails=3 fail_timeout=30s;
server 10.0.0.12:80 max_fails=3 fail_timeout=30s;
}
location / {
proxy_pass http://laravel_web;
proxy_set_header Host $host;
}
The default method is round-robin. least_conn sends each request to the server with the fewest active connections, which I'd pick when some requests (exports, reports) take much longer than others.
You'll see ip_hash recommended for "sticky sessions". I'd skip it for Laravel. If sessions break across servers, the real fix is moving SESSION_DRIVER from file to redis or database, not pinning users to one box.
Keepalive connections to the backend
By default nginx opens a new TCP connection to the backend for every request and closes it afterwards. The keepalive directive only exists inside an upstream block, so this is the one benefit you can't get any other way:
upstream octane {
server 127.0.0.1:8000;
keepalive 16;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://octane;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
}
}
The two extra lines matter. Without proxy_http_version 1.1 and an empty Connection header, nginx keeps sending Connection: close and the keepalive pool does nothing. For PHP-FPM the equivalent is fastcgi_keep_conn on; alongside keepalive in the upstream.
The number 16 isn't magic. It's the count of idle connections each worker keeps, not a limit on total connections. I wouldn't tune it until you've measured something.
Failover and the "no live upstreams" error
With a named block you get passive health checks. max_fails=3 fail_timeout=30s means three failed attempts within 30 seconds mark the server unavailable for 30 seconds. Add a fallback with backup:
upstream laravel_web {
server 10.0.0.11:80 max_fails=3 fail_timeout=30s;
server 10.0.0.12:80 backup;
}
When every server is marked down, you get no live upstreams while connecting to upstream in the log and a 502 for the visitor.
Two gotchas worth knowing:
- With a single server in the group, nginx ignores
max_failsandfail_timeoutand never marks it down. Failover only means something with two or more servers. proxy_next_upstreamretries failed requests on the next server, but by default it won't retry a POST once the request has been sent. That's deliberate, so a form submission isn't processed twice. Don't addnon_idempotentunless your endpoints can safely run twice.
Active health checks (nginx probing a URL on a schedule) aren't part of open-source nginx. If you read about health_check, that's NGINX Plus.
Docker containers and "host not found in upstream"
This is where the upstream block bites self-hosters. If you write server app:8000; or proxy_pass http://app:8000;, nginx resolves app once, at startup or reload. Two problems follow:
- If the container isn't up yet, nginx refuses to start with
[emerg] host not found in upstream "app:8000". - If the container is recreated and gets a new IP, nginx keeps sending traffic to the old one, and you get 502s until you reload.
The fix is a variable, which forces a lookup at request time through the resolver:
server {
listen 80;
resolver 127.0.0.11 valid=10s; # Docker's embedded DNS
location / {
set $app_backend http://app:8000;
proxy_pass $app_backend;
proxy_set_header Host $host;
}
}
The trade-off: no keepalive, no least_conn, no backup. For a single container on one VPS, I take that trade every time. Newer nginx releases also accept a resolve parameter on server lines inside an upstream block (it used to be NGINX Plus only), so check the docs for the version you actually run before relying on it.
If you're debugging which IP a hostname resolves to, testing it with curl against a specific DNS server is quick; setting a DNS server for a curl request shows how.
Readability, reuse and the Host header gotcha
A named block keeps the backend address in one place. If five location blocks proxy to the same app, change it once instead of five times.
The gotcha: by default nginx sends Host: $proxy_host to the backend, and with a named upstream that's the name of the block. Your Laravel app receives Host: laravel_web, which breaks name-based virtual hosts on the backend and can generate wrong absolute URLs. Always set:
proxy_set_header Host $host;
While you're in there, pass the client IP too, otherwise every request looks like it came from nginx. The X-Real-IP header fix for nginx covers that end to end.
One more mismatch I see often: proxy_pass https://... pointed at a backend that only speaks HTTP. The log shows SSL_do_handshake() failed ... wrong version number while SSL handshaking to upstream. It's the same root cause as curl (35) wrong version number on nginx: a scheme that doesn't match the port.
Which option to use for your setup
- Laravel with PHP-FPM on the same VPS: inline
fastcgi_pass unix:/run/php/php-fpm.sock;is fine. An upstream block adds nothing unless you want FastCGI keepalive. - Octane, Node or any long-running app on localhost: named upstream with
keepalive. This is the case where the block pays off most. - Two or more app servers: named upstream,
least_conn,max_fails, and shared sessions in Redis. - Docker containers that get recreated: variable plus
resolver 127.0.0.11. Accept losing keepalive. - External host with changing IPs (a cloud load balancer, a SaaS API): variable plus a public
resolver, for the same reason.
If your setup mixes several of these and the 502s won't go away, HeadsNTail services can take a look at the config with you.
FAQ
What is the difference between upstream and downstream in nginx?
Upstream is the server nginx forwards requests to, such as PHP-FPM or an app server. Downstream is the client sending requests to nginx, usually a browser or another proxy. Error log messages say "upstream" when the backend is the side that failed.
What does "upstream timed out" mean in nginx?
It means nginx connected to the backend but didn't get a response in time. The message usually ends with while reading response header from upstream, which points at proxy_read_timeout (or fastcgi_read_timeout for PHP-FPM). Raising the timeout hides the symptom, so check why the backend is slow first.
Where do I put the upstream block in nginx?
In the http context, not inside server or location. On Debian and Ubuntu, a file in /etc/nginx/conf.d/ works because it's included inside http. Putting it inside a server block gives "upstream" directive is not allowed here.
Do I need an upstream block for PHP-FPM?
No. fastcgi_pass unix:/run/php/php-fpm.sock; works on its own. Use an upstream block only if you want FastCGI keepalive or several FPM pools behind one name.
What does "no live upstreams" mean?
Every server in the upstream group has been marked unavailable after failing max_fails times within fail_timeout. Check that the backends are actually running, then look at the earlier error lines to see why the first attempts failed.
Now you know what does upstream mean in nginx, open your error log and match the exact "upstream" message to the section above.


