live broadcast · the Web Servers GP, stage by stage
Web Servers narrated
live from the paddock.
Good afternoon, everyone! The engine's already warm, and today there's only one fight: building the pit wall that keeps the driver clear of the chaos outside. Five stages, from what a web server is for to the event loop that lets Nginx dominate any edge. Lights out, the driver takes over the narration.
Every web server concept read the way a driver reads telemetry: the problem each piece solves, a technically honest F1 analogy, the config commented line by line, commands with the expected output, and a hands-on section per stage.
The cheat sheet: pin this to your visor
The same analogy holds from the first stage to the last. Don't try to memorize this table now. It's the track map, not the exam: go straight to Stage 01 and come back here whenever an analogy shows up in the text.
| Software concept | Formula 1 |
|---|---|
| Application (business logic, Node on port 3000) | The driver driving: the core skill, race and nothing else |
| Web server (Nginx / Apache) | The whole pit wall and garage operation: everything around the driver |
| HTTP request | A radio call, a request reaching the pit wall |
| Connection | A car in the team's care, in the garage or on track |
| Static content | Ready-made data handed over without bothering the driver (track maps, lap references) |
| Proxy to a dynamic app | Passing the driver the one question only he or the engineer can answer |
| Reverse proxy | The pit wall at the front door: takes everything from outside and routes it to the cars behind it |
| Forward proxy | The driver's manager, speaking to the outside world on his behalf (outbound) |
| Load balancer | The strategist splitting load and stints across the team's cars |
| TLS/SSL termination | The encrypted radio channel, managed at one single point by the team |
| Cache | The engineer's ready-made answers for the questions that always come back |
| Access control and security | The paddock pass and the box security guard, keeping out anyone without clearance |
| HTTP (the protocol) | The radio and telemetry protocol between car and pit wall |
| Stateless HTTP | Every radio call stands alone, context lives on the pit board (cookies, sessions) |
| Head-of-line blocking | A jammed radio queue: one long transmission holds up every other one |
| HTTP/1.1 keep-alive | Keeping the radio channel open, but one transmission at a time |
| HTTP/2 multiplexing | Several telemetry channels running at once over the same link |
| HTTP/3 (QUIC over UDP) | Swapping the transport so a lost packet doesn't stall the rest (a bad radio zone) |
| MPM (Apache) | How the team scales its crew of mechanics in the garage |
| Process (Apache) | An independent box, with its own tools and its own space |
| Thread | A mechanic inside that box |
| Shared process memory | The shared toolbench inside that box |
| Prefork MPM | One whole, isolated box for every single car: total isolation, sky-high cost |
| Worker MPM | A few boxes, each with several mechanics sharing the same toolbench |
| Event MPM | Mechanics who don't stand still babysitting a car that's just waiting |
| Non-thread-safe module | A legacy tool that can't share a toolbench: it needs its own isolated box |
| Event-driven architecture (Nginx) | The pit wall running an event loop: a handful of engineers watching the whole telemetry wall |
| Non-blocking I/O | The engineer logs "let me know when car X's data comes in" and keeps working the others |
| Event loop | The engineer sweeping the telemetry board, acting only when an alert fires |
| Worker process (Nginx) | Each engineer posted at the telemetry wall |
| epoll | The telemetry system that flags exactly which channel just got new data |
Where the analogies break down (a driver who hides the car's limits crashes into the wall):
- Connection as a car, in Stage 04 and 05: it works for grasping cost and isolation, but a process or a thread spins up and dies in milliseconds and you have thousands of them. A real car is expensive, one of a kind, and you only get two per team. The scale is completely different: where the garage has 2 cars, the server has 4,000 connections.
- The engineer at the telemetry wall, in Stage 05: it works for explaining the event loop, but a human gets tired and loses track; Nginx's worker doesn't. Nginx's real bottleneck is CPU and memory, not attention span. The image is useful for teaching, the mechanism is different.
- Radio and telemetry as HTTP, in Stage 02: the head-of-line blocking parallel holds up, but F1's real radio has human priority (the engineer chooses when to speak) and the protocol doesn't. F1 telemetry is also mostly one-way (car to pit wall), while HTTP is always a request-response pair. Use the image to feel the queue and the lost packet, not to equate the protocols byte for byte.
Stage 01 pit wall + garage
What a web server is for
Lights out! The very first corner already asks the question that decides the whole race: what stays in the driver's lap, and what goes to the pit wall. The best teams win Grands Prix in the pit box, and a Red Bull stop under two seconds proves the operation around the driver matters as much as the car. Let's find out why.
Picture a team that spins up a Node.js app and lets it answer directly on port 3000, with nothing in front of it. At first it works fine, but the product grows and a string of pains show up, none of them related to business logic: serving static files starts competing for the same process running the app, renewing the HTTPS certificate means touching the code and restarting the process, spreading traffic across replicas turns into a manual call on every request, and there's no layer filtering bots before they hit the application directly. Translated to the cockpit: it's the driver who, on top of driving, would also have to run his own encrypted radio, decide the stint strategy alone, and still keep intruders out of the box. Nobody races like that.
How it works
Every one of these problems is edge infrastructure, not business logic. Solving all of them inside the application's own code mixes up responsibilities that should stay separate, and that exact set of responsibilities is what a web server like the Apache HTTP Server or Nginx takes on.
The analogy: think of your team's pit wall and garage. Without them, the driver would have to work out strategy alone, handle his own secure comms, and still watch who walks into the box. With a central operation, everything arrives at one point (the pit wall), which decides what to pass on to the driver, turns away anyone without clearance, and handles the shared chores (radio, telemetry, timing) without breaking the concentration of whoever's in the car. The web server is that pit wall for your infrastructure: the point every HTTP request passes through before it reaches (or doesn't) your application.
A modern web server typically takes on the six functions in the cards below. Worth remembering: this piece of software is a war veteran. The Apache HTTP Server first shipped in 1995, which makes it more than 30 years on track.
The six functions of a modern web server
Serving static content
HTML, CSS, JS, images and video straight from disk, no application code involved. The track map handed over without calling the driver on the radio.
Proxying to a dynamic app
Forwards requests to PHP-FPM, Tomcat, Node.js, Gunicorn, and the like. The pit wall passing the driver the one question only he can answer.
Load balancing
Spreads requests across multiple application servers. The strategist splitting load, lap after lap, between the team's cars.
SSL/TLS termination
Manages certificates and encryption at one central point instead of spreading it across every instance. The team's one encrypted radio channel.
Caching
Keeps already-processed responses to serve faster next time, without repeating the work. The engineer with the answer ready for the question that always comes back.
Access control and security
Filters malicious traffic and applies authentication before an attack gets anywhere near the application. The paddock pass at the box door.
The Server header gives away who's answering you
# /etc/nginx/conf.d/default.confserver {listen 80; # the port Nginx listens onserver_name localhost; # the domain this block serveslocation / {root /usr/share/nginx/html; # the folder holding the static filesindex index.html; # file served when the URL ends in "/"}}
💡 This block is illustrative: it shows the syntax, but you don't need to create or edit it by hand to complete this stage's hands-on. The official nginx:latest image already ships with an equivalent config, serving a static page on its own. The car rolls off the truck with the base setup already fitted.
Essential commands for this stage
Hands-on
Two containers up, a curl to read the Server header, and you can already feel the pit wall answering before the driver gets in the car. No rush to set a lap time here: it's a track familiarization lap.
- Spin up an Nginx container (docker run -d -p 8080:80 nginx:latest) and an Apache container (docker run -d -p 8081:80 httpd:latest).
- Run curl -I http://localhost:8080 and curl -I http://localhost:8081. Notice the Server header on each: two different pieces of software answering the same job. Two different pit wall vendors doing the same work.
- Ask for a path that doesn't exist on each one (curl -I http://localhost:8080/does-not-exist) and notice the 404 Not Found. That response came from the web server, with no application ever triggered: the pit wall turned the request away at the door, the driver never even knew it existed.
Tip: Always use curl -I to inspect just the headers, without downloading the whole response body: it's the fastest way to find out which server is actually answering before you dig deeper.
Takeaway: A web server doesn't run your application's business logic: it handles everything around it (static files, proxying, TLS, cache, security) so the application doesn't have to worry about any of it. Just like the pit wall exists so you can just drive.
Clear as day: the pit wall exists so you can just drive. Now the track opens onto the longest straight of the circuit, the evolution of the radio protocol between car and box. Stage 02, and it's all about shaving milliseconds off the conversation.
Stage 02 the HTTP evolution straight
HTTP and its evolution, from 1.1 to QUIC/HTTP-3
Here the fight is over milliseconds in the conversation between car and pit wall, and three generations of protocol are racing on the same tarmac. Remember Mansell glued to Senna's gearbox in the closing laps of Monaco 1992, faster and unable to pass? Head-of-line blocking is exactly that: the quick one stuck behind whoever got there first.
Two scenes explain why HTTP had to evolve, framed as two communication failures between car and pit wall. Scene 1: an e-commerce page loads the main HTML plus 40 thumbnail images; even with persistent connections (keep-alive), HTTP/1.1 processes one request at a time within that connection, so the second image only starts downloading once the first has arrived in full. It's a single radio: while the engineer dumps one long transmission, every other message waits in line. Scene 2: even once scene 1 is fixed, a user on a shaky mobile network still feels the lag, because a single lost packet along the way is enough to freeze the whole page load until the retransmission arrives, even though everything else already got there. It's the car entering the Monaco tunnel: it loses one word of the radio and the whole message freezes until it repeats, even though the rest had already come through.
How it works
Before the version numbers, one structural fact about HTTP itself: it's a stateless protocol. Every request is handled completely independently, with no memory of previous requests, like an isolated radio call that carries no memory of the calls before it. That makes the protocol simpler to scale, but it requires mechanisms layered on top (cookies, sessions) to fake context between requests from the same user. On the pit wall, that context lives on the pit board and in accumulated telemetry, not in the radio call itself.
HTTP/1.1, the classic version (1997): introduced persistent connections (keep-alive), letting multiple requests and responses share the same TCP connection, which cut a lot of the overhead of opening a fresh connection for every resource. The catch is head-of-line blocking: if the first request takes a while, everything behind it, on that same connection, has to wait. Channel stays open, sure, but one transmission at a time, and the long one holds up the queue.
HTTP/2, multiplexing (2015): solves scene 1's head-of-line blocking at the application layer, multiple requests and responses travel simultaneously over the same TCP connection, with none blocking another. It's the modern telemetry of a 2026 car, dumping dozens of channels at once over the same link (tire pressure, ERS deployment, DRS mode) with no channel holding up another. It also brought resource prioritization, header compression via HPACK, and Server Push (a feature that shipped in the original spec but has since fallen out of use: the major browsers have removed or restricted support for it since 2022, because the real performance gain rarely justified the complexity of using it correctly).
HTTP/3, QUIC over UDP: HTTP/2 solved head-of-line blocking at the application layer, but scene 2's problem still lives one layer down. TCP guarantees bytes arrive in strict order, so if a packet is lost, everything that came after it waits for the retransmission, even if it belongs to a different stream than the one that stalled. HTTP/3 attacks that by swapping TCP for QUIC (Quick UDP Internet Connections), which runs over UDP: every stream is independent, and losing a packet from one stream doesn't stall the others. It's like having every telemetry channel immune to the others' noise: if the brake-temperature reading drops out for an instant in the tunnel, the GPS channel doesn't even notice. The gain is biggest on networks with packet loss or high latency, the typical case for mobile connections, in other words the car out in the far, noisy stretches of a long track like Spa.
The three generations, side by side
HTTP/1.1 (1997)
TCP transport. Fixed reopening a connection for every request with keep-alive. Limitation: head-of-line blocking, one transmission at a time.
HTTP/2 (2015)
Still TCP. Fixed application-layer head-of-line blocking with stream multiplexing over one connection, plus HPACK and prioritization.
HTTP/3 (QUIC)
UDP transport via QUIC. Fixed TCP's own head-of-line blocking with independent streams: a lost packet only stalls its own stream.
Asking curl itself which version got negotiated
# Let curl negotiate freely and show the version used in the response$ curl -so /dev/null -w 'Negotiated protocol: HTTP/%{http_version}\n' https://www.cloudflare.comNegotiated protocol: HTTP/2# Force HTTP/1.1, even if the server supports newer versions$ curl --http1.1 -so /dev/null -w 'Negotiated protocol: HTTP/%{http_version}\n' https://www.cloudflare.comNegotiated protocol: HTTP/1.1# Force HTTP/3 (needs a curl build with QUIC support)$ curl --http3 -so /dev/null -w 'Negotiated protocol: HTTP/%{http_version}\n' https://www.cloudflare.comNegotiated protocol: HTTP/3
💡 Not every curl build ships with HTTP/3 support (it depends on a QUIC library, like ngtcp2 or quiche). Run curl --version | grep -i http3 to check before using the --http3 flag, the same care as checking the car has the QUIC telemetry kit installed before you demand that mode.
Essential commands for this stage
Hands-on
Three curl commands, three protocol versions negotiated right in front of you. Clock the %{time_total} of each and feel, in your own hands, the difference every generation brought. This one's a qualifying lap.
- Pick a domain you know supports HTTP/2 (for example, www.cloudflare.com or www.google.com) and run the three commands from the example above. Watch the %{http_version} value change with the flag used.
- Run curl -v --http2 https://www.cloudflare.com 2>&1 | grep -i "HTTP/2" and find, buried in the verbose output, the line confirming the version negotiated during the handshake. It's the radio handshake before the first real transmission.
- If your curl supports HTTP/3, repeat with --http3 and compare the response time with -w "%{time_total}\n" across all three variants.
Tip: Realizing HTTP/2 cleared the queue up at the application layer and HTTP/3 dropped down to the transport layer to kill the problem at the root is the kind of distinction a senior engineer makes without even thinking. Catch that difference and you're already running with the front pack.
Takeaway: Every HTTP version solved head-of-line blocking at a different layer: /2 solved it at the application layer with multiplexing, /3 solved it at the transport layer, swapping TCP for QUIC over UDP. One cleared the radio queue, the other made every telemetry channel immune to its neighbors' noise.
You watched the radio clear its queue and the telemetry go deaf to interference. Now the circuit stretches into a technical chicane: two lookalike intermediaries doing opposite jobs. Stage 03, mind the braking zone.
Stage 03 the proxy chicane
Forward Proxy and Reverse Proxy, the web's two intermediaries
Careful with this sequence: forward proxy and reverse proxy are twins running in opposite directions. Get them mixed up here and you lose the car in the gravel. Full focus through the next few corners.
Two mirrored scenes, one on the client side and one on the server side. Think of a car that needs to talk to the world (outbound) and a box that needs to receive the world (inbound). Scene A: a company wants every outbound request from its employees to pass through a single control point, to block unwanted sites, log access for compliance, and stop each machine's internal IP from being exposed straight to the internet. Scene B: the same company runs five application servers behind a single public domain, and it makes no sense to expose each server's IP individually, repeat the TLS certificate setup on every one of them, or let each server decide on its own whether it can handle one more simultaneous connection. Two different needs, but with one piece in common: an intermediary sitting between whoever's asking and whoever's answering. The difference is which side that intermediary represents and protects.
How it works
Forward Proxy, the outbound intermediary: sits between a client (usually inside a private network) and the internet, and is explicitly configured on the client's side (in the browser, the operating system, or the application). Analogy: it's the driver's manager. The driver doesn't personally go negotiate with a sponsor or field questions from the press, he talks to his manager, who decides what goes out, negotiates on his behalf, and brings the outcome back. The sponsor on the other end has no idea the words came from the driver, all they see is the manager. The driver hired that intermediary on purpose, exactly like a client configures a forward proxy. Use cases: security and compliance (routing traffic through firewalls), content control (blocking sites), anonymity and privacy (masking the client's real IP), and caching (storing frequent responses to save bandwidth).
Reverse Proxy, the smart gatekeeper at the front door: sits at the edge of the infrastructure, receiving requests from the internet and handing them out to the application servers behind it. It's transparent to the client, who has no idea it's even there. Analogy: it's literally the pit wall from Stage 01, seen from another angle, now formalized specifically for when it exists to protect and organize what's behind it. Picture Ferrari's pit wall taking every incoming call from outside and deciding, request by request, whether it's business for Hamilton's car or Leclerc's, already armed with ready answers for the routine stuff and turning away anyone who shouldn't get near the cars. Main functions: load balancing across multiple backends, centralized SSL/TLS termination (taking that CPU load off the backends), content caching, a first line of defense against malicious traffic (blocking specific attacks like XSS usually needs extra WAF rules, not just the plain proxy), compression (Gzip/Brotli), and smart routing across different services.
Summed up in paddock slang: forward proxy is the manager speaking for the driver on the way out, reverse proxy is the pit wall taking in the world on the way in. Same kind of intermediary, opposite directions.
The two sides of the same mechanism
Forward Proxy
Sits on the client's side, configured by the client itself. Protects the client against the internet. Visible and explicit: you point at it on purpose.
Reverse Proxy
Sits on the server's side, configured by the server's own infrastructure. Protects the server against the internet. Transparent: the client has no idea it's there.
Reverse Proxy's functions
Load balancing, SSL/TLS termination, caching, a first security layer (real XSS defense needs a separate WAF), compression, and routing across services.
An Nginx reverse proxy in front of a simple backend
# /etc/nginx/conf.d/proxy.confupstream backend_app {server 127.0.0.1:8000; # the real application server, behind the proxy}server {listen 80;server_name localhost;location / {proxy_pass http://backend_app; # forwards the request to the backendproxy_set_header Host $host; # preserves the client's original Hostproxy_set_header X-Real-IP $remote_addr; # tells the backend the client's real IPproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # chain of IPs, if there's more than one proxy}}
💡 Without the X-Real-IP/X-Forwarded-For headers, the backend would see every request as coming from Nginx itself (127.0.0.1), losing track of who actually made the request. It'd be the pit wall relaying a message to the driver without saying who it came from.
Essential commands for this stage
Hands-on
Spin up the Python backend, put Nginx in front of it, reload, and check the Server header with a curl: every step is a tire change, do it in order and the car leaves the box clean. Garage warning: this exercise edits Nginx's real config and reloads the process with nginx -s reload, so use an Nginx installed on the host (sudo apt install nginx), not the nginx:latest container from Stage 01. If you'd rather stay on Docker, see the workaround suggested at the top of the guide.
- Spin up the Python backend (python3 -m http.server 8000, in a folder with any index.html) and configure Nginx as in the example above. Reload with nginx -s reload.
- Run curl -I http://localhost and notice the Server: nginx header, even with the content coming from the Python process. The pit wall out front, the car behind it.
- Temporarily remove the proxy_set_header X-Real-IP line, reload, and think through what the backend would now see: without that header, it would only see Nginx's own IP.
- Compare with the forward proxy's curl -x command: notice that there, it's you pointing at the intermediary on purpose; in step 2, you didn't even know it existed.
Tip: To tell whether you're on the explicit or the implicit side of a proxy, ask: who set up the pointer, the client or the server? That answer alone tells you whether it's forward or reverse.
Takeaway: A forward proxy sits on the client's side and is configured by it; a reverse proxy sits on the server's side and is invisible to the client, but both are the same kind of intermediary, looking in opposite directions. Manager on the way out, pit wall on the way in.
Manager on the way out, pit wall on the way in: you won't mix those up again. Now comes the heaviest braking zone on the track, where Apache's garage strategy decides who survives the peak. Stage 04.
Stage 04 the MPM braking zone
Apache HTTP Server and its processing models (MPMs)
Here the track demands garage strategy: how do you scale your crew of mechanics to handle 5,000 cars wanting the box at the same time? Get this wrong and it costs you. Ask Ferrari about Abu Dhabi 2010, who threw away a championship on the wrong box call at the wrong moment.
Scenario 1: an e-commerce site running Apache with the oldest processing model takes a spike of 5,000 simultaneous connections during a sale. Since every connection gets its own, fully isolated operating-system process, the server tries to open thousands of processes, each burning tens of megabytes just to exist, before it's even processed a single request. Memory runs out, the system starts swapping to disk, and the site crashes right at the sales peak. It's a pit-stop window under a safety car: all 22 cars want the box at the same time, and if you demanded a whole separate crew for every car, there wouldn't be room or people in the pit lane for all of them. Scenario 2, the opposite: the same company depends on an old module written with no concurrency care (not thread-safe), and it has to keep working. Switching the processing model to one based on threads sharing memory would make that module corrupt data or crash the process unpredictably. It's that legacy calibration tool that only works on an isolated bench: on a shared bench, it messes up every other mechanic's work. One scenario calls for lightness to scale; the other calls for total isolation to survive. Apache resolves that tension by letting you choose the concurrency model, the MPMs (Multi-Processing Modules).
How it works
General analogy: think of the garage and how you scale the mechanics. In this stage, a connection is a car pulling in to be serviced, a process is an independent box with its own tools and its own space, and a thread is a mechanic inside that box, sharing the same toolbench.
Prefork MPM (non-threaded): an independent child process gets spun up for every connection, fully isolated from the rest. Advantages: very stable (a problem in one process doesn't touch the others), ideal for non-thread-safe modules, high compatibility with legacy software. Drawbacks: high memory use and poor scalability under heavy concurrency. Prefork commonly pairs with mod_php (PHP embedded inside Apache's own process, historically not thread-safe); the modern alternative is running PHP as a separate process via PHP-FPM, with Apache talking to it over a proxy, which lets you run a lighter MPM without losing compatibility.
Worker MPM (multithreaded): spins up multiple child processes, each running multiple threads, and each thread handles one connection. More memory-efficient than Prefork and better performing under heavy concurrency, but since threads in the same process share memory, a serious bug in one thread can corrupt the whole child process hosting it, taking down the connections that specific process was serving (the other processes and the master process stay up). It's the mechanic who knocks over the toolbench and only hurts his own box's crew; the other boxes and the team principal keep working. That's why the modules need to be thread-safe.
Event MPM (recommended for production): builds on the Worker model, but tuned for the common case of open, idle keep-alive connections: one dedicated thread can watch several connections that are just waiting, without tying up a whole working thread just to hold an idle connection open. It's the mechanic who doesn't stand rooted next to a car that's only waiting for an order; he watches several waiting cars at once and only acts when one of them actually needs him. Efficient for keep-alive, tuned for high concurrency, with native HTTP/2 support, but it demands thread-safe modules and is more complex to configure.
The three MPMs, side by side
Prefork
Total isolation, one process per connection. High memory, low concurrency. Use for: maximum stability, non-thread-safe modules (e.g., legacy mod_php).
Worker
Few processes, threads sharing memory. Medium memory, high concurrency. Use for: moderate-to-high traffic, requires thread-safe modules.
Event
Like Worker, but tuned for idle keep-alive. Low memory, very high concurrency. Use for: recommended for most new deployments.
Finding and switching the active MPM
# Shows which MPM is compiled in / active right now (trimmed output)$ apachectl -V | grep -i mpmServer MPM: event# On Debian/Ubuntu, a2query shows which MPM module is enabled$ sudo a2query -Mevent
Essential commands for this stage
Hands-on
Find the active MPM, switch it, and run Apache Bench before and after: the same machine stopping in 2 seconds or 4, just from how you scaled the crew. Garage warning: this exercise uses a2enmod/a2dismod and apachectl, commands that act on an Apache installed on the host (sudo apt install apache2), not on the httpd:latest container from Stage 01. Step 3 also uses ab: if it's not installed, run sudo apt install apache2-utils before you get there.
- Run apachectl -V | grep -i mpm (or httpd -V | grep -i mpm, depending on the distro) on your Apache and note which MPM is active right now.
- If your system is Debian/Ubuntu, switch the active MPM with a2dismod/a2enmod and confirm the switch with a2query -M.
- Run a simple load test against the same Apache, before and after the MPM switch: ab -n 500 -c 50 http://localhost/. Compare the Requests per second and Time per request lines between the two runs. Don't expect a magic number: the point is to feel that the same machine behaves differently just because of the MPM chosen, the same feeling as a pit stop that comes out in 2 seconds or in 4 depending on how the crew was scaled.
Tip: Prefork, Worker, Event: memorizing the three names gets you to one level; understanding that the difference lives in process, thread, and isolation gets you to another. Worth rereading the table slowly before you decide which to run in production.
Takeaway: Prefork isolates everything into processes and is the heaviest; Worker splits that into threads inside a handful of processes and is lighter, but demands thread-safe code; Event is Worker tuned so it doesn't burn a whole thread on an idle keep-alive connection, and it's the recommended model today. An isolated box per car, a shared bench per crew, or a mechanic watching several waiting cars at once.
An isolated box per car, a shared bench per crew, or a mechanic watching several waiting cars at once: you can already pick the garage layout to match the race. All that's left is the final fast corner, where Nginx shows why it was born for this era. Stage 05.
Stage 05 the event-loop fast corner
Nginx and its event-driven architecture
The final split of the lap, and this is where the grid's leanest chassis shines. Nginx follows F1 designers' golden rule: simplify first, then strip out every gram of weight that doesn't need to be there. Let's watch this car through its best corner.
Scenario: an application with lots of long-lived, mostly idle simultaneous connections (real-time chat, push notifications, keep-alive for mobile app APIs). Even with Apache's Event MPM cutting the cost of idle connections, the model is still built on reserving one thread for every connection being actively processed. Scaling to tens of thousands of simultaneous connections means scaling, in the same proportion, the number of threads and the memory they eat, until you hit the practical limit of how many threads can coexist efficiently. Imagine wanting one dedicated engineer per car to monitor tens of thousands of cars at once: no pit wall can hold that. That's the exact problem Nginx was built from scratch to solve. While Apache started life as a traditional web server and picked up concurrency models over time, Nginx was designed from day one with a modern architecture in mind, built specifically to serve static content and act as a reverse proxy under heavy concurrency.
How it works
Nginx's core difference is its asynchronous, event-driven architecture, quite unlike the dedicated thread- or process-per-connection model. Analogy: picture a race engineer at the telemetry wall, the giant board showing every car's telemetry at once. He doesn't stand there staring at a single car waiting for something to happen. He sweeps the whole board and only acts when an event fires: a tire-temperature alert on Russell's car, an undercut request from Antonelli's. One engineer can monitor the entire grid this way, because he's never blocked waiting on just one car.
On Apache, every active connection tends to hold a dedicated thread or process for as long as it lasts (even the Event MPM only reserves a thread for the request being actively processed, it just avoids spending a whole thread on connections that are merely idle). With 10,000 users connected at once, you need to sustain a meaningful fraction of 10,000 threads or processes eating memory. It would be like wanting one engineer per car for a grid of ten thousand cars.
On Nginx, non-blocking I/O: when an I/O operation is needed (reading from disk, waiting on a backend's response), the worker registers the event and moves on to another connection, instead of standing there waiting. The engineer logs "let me know when car X's data comes in" and goes straight to watching car Y. The event loop is the loop each worker process runs, handling thousands of connections with few resources, the continuous sweep of the telemetry wall. When the event is ready, the worker gets notified and picks up exactly where it left off: the alert flashes on the board, the engineer reacts on that car, and goes back to sweeping the rest.
This approach makes Nginx extremely efficient, especially for keep-alive and idle connections, exactly this stage's scenario. Practical benefits: lower memory use, higher throughput, better latency, and excellent performance as a reverse proxy and load balancer, including in front of Apache itself.
One caveat, so as not to oversell it: at a real telemetry wall, a human engineer has limited attention and genuinely loses track with too many cars. Nginx's worker doesn't get tired: it processes every event in microseconds and goes back to the loop, and the bottleneck becomes CPU and memory, not attention. The engineer image is there to explain the event loop's logic, not to claim there's a human inside making it happen.
The pieces of the event-driven engine
Non-blocking I/O
Instead of standing idle waiting on a disk or backend response, the worker registers the event and moves on to another connection.
Event loop
Every worker runs a loop continuously sweeping connections, acting only when an event fires.
epoll
A Linux kernel mechanism that tells the worker exactly which connection just got new data, no blind sweeping needed.
Few processes, thousands of connections
# /etc/nginx/nginx.conf (main excerpt)worker_processes auto; # 1 worker process per available CPU coreevents {worker_connections 1024; # each worker can handle up to 1024 simultaneous connectionsuse epoll; # Linux kernel's event notification mechanism}
💡 With worker_processes auto and worker_connections 1024 on a 4-core machine, Nginx can, in theory, serve up to 4,096 simultaneous connections using just 4 worker processes (plus the master process, which only manages the workers). Four engineers at the telemetry wall watching 4,096 cars, and a team principal coordinating the engineers without ever picking up a radio.
Essential commands for this stage
Hands-on
Count the processes with ps aux, throw load at it with ab, and watch: the number of engineers at the wall doesn't budge, no matter how many cars show up. Garage warning: just like in Stage 03, this exercise reads config and watches processes for an Nginx installed on the host, not the nginx:latest container from Stage 01. Step 3 uses ab again: install it with sudo apt install apache2-utils if you haven't already in Stage 04.
- Find your Nginx's nginx.conf (nginx -t shows the path at the top of its output) and locate the worker_processes and worker_connections directives.
- Run ps aux | grep '[n]ginx' and count how many processes show up. Compare with nproc (your machine's core count): the worker count is usually equal to or close to it. As many engineers as available cores.
- Generate load with several simultaneous connections: ab -n 1000 -c 200 http://localhost/. While the test runs, run ps aux | grep '[n]ginx' in a second terminal. Notice: the process count doesn't change, even with hundreds of simultaneous connections hitting the server. The same engineers at the wall, whether more cars show up or not.
- If you have access to an Apache for the same kind of test, repeat the load there and run ps aux | grep '[a]pache2\|[h]ttpd'. Depending on the configured MPM, notice the process or thread count grow along with the load, unlike what happened with Nginx. There, the garage hires mechanics as traffic picks up; here, the wall keeps running with the same crew.
Tip: Non-blocking I/O and the event loop are the heart of it all: a single engineer sweeps the whole telemetry wall without ever getting stuck on one car. Get that, and you get why Nginx scales the way it does.
Takeaway: Apache handles concurrency by dedicating a process or thread to every connection (even when tuned by the Event MPM); Nginx handles concurrency with a handful of processes running a non-blocking event loop, and that's exactly why it dominates as a reverse proxy and load balancer in front of anything, including Apache itself. One mechanic per car versus one engineer sweeping the whole telemetry wall.
A handful of processes, one event loop, and Nginx dominating the edge of anything, including Apache itself. You've crossed the whole track, from static content to the event loop. Pack up the pit board, go over the debrief at your own pace, and see you at the next race on the calendar.