Web Server vs. Application Server: Core roles
Same hardware, two opposite jobs. The split is about workload, not machine.
Image backend outage (OpenRouter HTTP 402, local fallback HTTP 502) — text-only card for this edition.
The real split: two kinds of workload, not two machines
- The boundary is about what work is worth which process: cheap I/O versus expensive compute.
- Static serving wants cache-friendliness, keep-alives, and cheap concurrency.
- App work wants process pools, warm JIT / connection caches, and isolation from slow-client stalls.
- One box doing both: a spike on the cheap path starves the expensive one — which is why the split survives even though one binary (Node, Caddy, Django runserver) can technically do both.
Web server vs. application server
- Web server: request/response over HTTP for bytes that already exist — map a URL to a file, slap on headers, ship it. No application code runs, so per-request cost is essentially I/O (nginx static is mostly
sendfile()syscalls) and one modest box can serve tens of thousands. - Application server: runs code per request — parse input, apply business rules, query databases / back-ends, assemble a response. Each request may cost milliseconds of CPU plus a round of downstream calls, so it is capacity-limited by logic, not bytes.
- The forward / dynamic-response arrows in the classic two-box diagram are the three-tier shape; the reason the boxes exist is that the two workloads have opposite scaling and failure profiles.
Falsify the split
- Serve the same site two ways on identical hardware: (1) one nginx serving a static HTML file, (2) the same HTML emitted by an app server that “runs logic” to produce it byte-identical.
- Measure requests/sec and p99 latency.
- If the gap is not 2–3 orders of magnitude, the “different workloads” justification for the split is decorative.
- If it is, the boundary earns its diagram.
Where the clean picture lies
- It’s a role, not a product: the same nginx can serve /static and reverse-proxy /api to gunicorn — production routes on path, so “web vs app server” is a question about a request, not a machine.
- “Static” is often generated: build-time rendering (Next.js SSG, Hugo) pre-bakes “dynamic” pages into files, so the app server runs zero times per request for most content — the modern split moved the boundary to build time.
- The box it omits: the reverse proxy / load balancer that terminates TLS, buffers slow clients, and decides static-vs-forward usually sits between them — the web server is often that proxy, and “forwards dynamic request” is a routing rule, not a handshake between peers.
Two kinds of workload, not two machines.