Comparisons

Web server vs application server: what each does, and how to wire them together

A web server speaks HTTP and serves files; an application server runs your code. Nginx vs PHP-FPM, Gunicorn and Tomcat, how they connect, and what breaks.

Published ~8 min read

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 serverApplication server
Main jobSpeak HTTP to the world, serve files, route requestsExecute application code and build responses
ContentStatic: HTML files, images, CSS, JS, downloadsDynamic: pages and API answers computed per request
Talks toBrowsers and clients over HTTP/1.1, HTTP/2, HTTP/3The web server over HTTP, FastCGI, uWSGI or a socket; databases and caches
ExamplesNginx, Apache httpd, Caddy, LiteSpeedPHP-FPM, Gunicorn, uWSGI, Uvicorn, Puma, Tomcat, Jetty, WildFly, Node.js
Good atThousands of connections, slow clients, TLS, caching, compressionRunning one language's code with its libraries and frameworks
Scales byEvent loops; one process handles many connectionsWorker 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 the sendfile system 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?

InterfaceLanguageTypical pair
FastCGIPHP (and others)Nginx fastcgi_pass → PHP-FPM
WSGI (PEP 3333)Python, synchronousNginx proxy_pass → Gunicorn or uWSGI → Django, Flask
ASGIPython, asynchronousNginx proxy_pass → Uvicorn or Hypercorn → FastAPI, Django Channels
RackRubyNginx proxy_pass → Puma → Rails
Servlets / Jakarta EEJavaNginx proxy_pass → Tomcat, Jetty, or Spring Boot's embedded server
Plain HTTPNode.js, Go, Rust, .NETNginx 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?

nginx · PHP through PHP-FPM (socket name differs by version: ls /run/php/)
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;
    }
}
nginx · a Python app on Gunicorn
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;
    }
}
The application server, started by systemd
# /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-failure

Gunicorn’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?

StatusWho says itUsual cause
502 Bad GatewayWeb serverApplication server down, wrong socket or port, or crashed mid-request
504 Gateway TimeoutWeb serverApplication server too slow — a long query, all workers busy
500 Internal Server ErrorUsually the applicationAn exception in your code; the details are in the app's log, not the web server's
503 Service UnavailableEitherOverloaded, in maintenance, or a rate limit answering
413 Request Entity Too LargeWeb serverUpload bigger than client_max_body_size (nginx) or the app's own limit
499 (nginx log only)Web serverThe 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.

Related articles

All comparisons