Short answer: a web server handles HTTP itself — it accepts connections, terminates TLS, serves static files from disk and passes everything else on. An application server runs your code — PHP, Python, Java, Ruby, JavaScript — and generates dynamic responses from business logic and databases. Nginx, Apache and Caddy are web servers; PHP-FPM, Gunicorn, Uvicorn, Puma and Tomcat are application servers. In most setups you run both: the web server in front, the application server behind it.
Web server vs application server at a glance
| Web server | Application server | |
|---|---|---|
| Main job | Speak HTTP to the world, serve files, route requests | Execute application code and build responses |
| Content | Static: HTML files, images, CSS, JS, downloads | Dynamic: pages and API answers computed per request |
| Talks to | Browsers and clients over HTTP/1.1, HTTP/2, HTTP/3 | The web server over HTTP, FastCGI, uWSGI or a socket; databases and caches |
| Examples | Nginx, Apache httpd, Caddy, LiteSpeed | PHP-FPM, Gunicorn, uWSGI, Uvicorn, Puma, Tomcat, Jetty, WildFly, Node.js |
| Good at | Thousands of connections, slow clients, TLS, caching, compression | Running one language's code with its libraries and frameworks |
| Scales by | Event loops; one process handles many connections | Worker processes or threads, limited by CPU and RAM |
How do they work together?
Follow two requests to the same site, served by Nginx in front of a Python app:
GET /static/app.css— Nginx finds the file on disk and sends it with thesendfilesystem call. The application never hears about it.POST /api/orders— Nginx buffers the request, forwards it to Gunicorn on a local socket, Gunicorn hands it to a worker running the Django or Flask app, the app queries PostgreSQL and returns JSON, and Nginx sends the answer back to the client — compressed, over TLS, at whatever speed the client can take.
The web server here is acting as a reverse proxy. That is the usual relationship: the web server is the front door, the application server one of the rooms behind it.
How does the web server talk to the application?
| Interface | Language | Typical pair |
|---|---|---|
| FastCGI | PHP (and others) | Nginx fastcgi_pass → PHP-FPM |
| WSGI (PEP 3333) | Python, synchronous | Nginx proxy_pass → Gunicorn or uWSGI → Django, Flask |
| ASGI | Python, asynchronous | Nginx proxy_pass → Uvicorn or Hypercorn → FastAPI, Django Channels |
| Rack | Ruby | Nginx proxy_pass → Puma → Rails |
| Servlets / Jakarta EE | Java | Nginx proxy_pass → Tomcat, Jetty, or Spring Boot's embedded server |
| Plain HTTP | Node.js, Go, Rust, .NET | Nginx proxy_pass → the app's own HTTP server |
WSGI, ASGI, Rack and the servlet API are contracts between an application server and the frameworks it runs, so the same Flask app can run under Gunicorn or uWSGI without changes. FastCGI and plain HTTP are how the web server reaches the application server. Node.js and Go blur the line: their standard libraries include an HTTP server, so the application is its own application server.
What does the wiring look like?
server {
listen 80;
server_name shop.example.com;
root /var/www/shop/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
}server {
listen 80;
server_name api.example.com;
location /static/ {
alias /srv/api/static/; # served by nginx, never by Python
}
location / {
proxy_pass http://unix:/run/gunicorn/api.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}# /etc/systemd/system/gunicorn-api.service (excerpt)
[Service]
User=api
WorkingDirectory=/srv/api
RuntimeDirectory=gunicorn
ExecStart=/srv/api/venv/bin/gunicorn --workers 5 --bind unix:/run/gunicorn/api.sock app.wsgi:application
Restart=on-failureGunicorn’s documentation suggests (2 × CPU cores) + 1 workers as a starting point — five on a two-core server — and each worker holds a copy of the application in RAM, so memory, not CPU, is often what limits the count on a small VPS.
Why not let the application server face the internet?
- Slow clients. A visitor on a bad mobile connection can take seconds to send a request or receive a response. In front of a handful of synchronous workers, a few such clients — or a deliberate slow-request attack — block the whole app. The web server buffers both directions, so workers only spend the milliseconds the code actually needs.
- Static files. Serving images through Python or PHP wastes a worker per file; the web server does it from the kernel.
- TLS, HTTP/2 and HTTP/3, compression in one well-tested place instead of in every app.
- Several apps on one IP — routing by host name and path to different application servers.
- Limits and protection — request size, rate limits, timeouts and IP rules before the app spends anything.
- Restarts without errors — the web server can hold or retry requests while an application server reloads.
When do you need only one of them?
A static site — plain HTML, or the output of a generator like Hugo or Astro — needs no application server at all; the web server serves files and that is the whole stack, which is why static sites are so cheap to host and so hard to take down. At the other end, an internal service called only by other servers on a private network can run on its application server alone, provided something else handles TLS and nothing public can reach it. Everything in between — any site with logins, forms, a database or an API — is better off with both, each doing what it is built for. Connection counts work differently on each side, too: an event-driven web server holds thousands of idle keep-alive connections for little memory, while each busy application worker handles one request at a time, so the web server is what absorbs traffic peaks and the worker count is what you size.
Is Apache a web server or an application server?
Both, depending on modules. With mod_php, Apache runs PHP inside its own worker processes, which makes it an application server too; with mod_proxy_fcgi and PHP-FPM, it is a web server in front of a separate one — the more common setup today. Tomcat can serve static files and was long used without a web server in front. The roles are what matter: something must speak HTTP to the world well, and something must run your code. On a small server they can be one process; as traffic grows, splitting them is the first step.
Which errors come from the boundary between them?
| Status | Who says it | Usual cause |
|---|---|---|
| 502 Bad Gateway | Web server | Application server down, wrong socket or port, or crashed mid-request |
| 504 Gateway Timeout | Web server | Application server too slow — a long query, all workers busy |
| 500 Internal Server Error | Usually the application | An exception in your code; the details are in the app's log, not the web server's |
| 503 Service Unavailable | Either | Overloaded, in maintenance, or a rate limit answering |
| 413 Request Entity Too Large | Web server | Upload bigger than client_max_body_size (nginx) or the app's own limit |
| 499 (nginx log only) | Web server | The client gave up before the app answered — often a slow app |
The first diagnostic question is always which side produced the error. The web server’s error log names upstream failures (connect() failed, upstream timed out); the application’s own log or its journal — for example journalctl -u gunicorn-api — shows the exceptions.
Running both on a VMHeaven VPS
A VPS with root access is the natural home for this pair: you choose the web server, the application server and their versions, and wire them up exactly as above. Size the plan by the application server — its worker count times the memory per worker, plus the database — rather than by the web server, which needs very little. Standard KVM gives the most vCores per euro for many workers; a fresh Ubuntu VPS or Debian VPS has Nginx, PHP-FPM and Python a single apt install away.
Frequently asked
Is Tomcat a web server or an application server?
An application server — a Java servlet container that runs Java web applications. It can serve static files and speak HTTP itself, but is usually put behind Nginx or Apache in production.
Do I need Nginx in front of Node.js or Gunicorn?
For anything public, yes. Nginx handles TLS, static files, slow clients, compression and limits far better, and lets several apps share ports 80 and 443; the application server stays on localhost.
Which one causes a 502 Bad Gateway?
The web server reports it, but the cause is almost always the application server behind it: not running, listening on another socket or port, or crashing while handling the request.