How HTTP servers work: the loop behind every response
A web server is not magic — it's a program in a loop: accept a connection, parse the request, match a route, run a handler, send a response. This guide walks that loop, static vs dynamic responses, and how servers survive many clients at once — with a simulator for each.
mindmap — quick refresh
Prerequisites: How HTTP works — you'll need requests, responses, and status codes.
Strip away the mystique and an HTTP server is a surprisingly small idea: a program that never exits, listening on a port, answering one question over and over — "here's a request; what's the response?" Everything else (frameworks, routing, middleware, nginx) is engineering layered on that loop.
If you know how HTTP messages look on the wire, you already know the server's input and output. This guide is about what happens in between.
The loop: accept → parse → route → respond
When you run a server on port 8000, the operating system starts delivering TCP connections to your program. For each one, the server reads the raw request text, parses out the method and path, looks up which piece of code should handle that path, runs it, and writes the response back:
Two things to notice. First, the router is just a lookup table — path patterns on one side, functions on the other, and a fall-through to 404 when nothing matches. Second, the 404 in the simulator isn't an error in the server — it's the server working correctly, telling the client its request matched nothing. (That's the 4xx = client's-side rule from the HTTP guide.)
Try a real server locally
Now turn the diagram into something you can visit. Python includes a small static-file server, so this experiment needs no package installation. It is a learning and development tool, not a production server.
1. Make a directory for the site
Open a terminal and run:
mkdir http-server-lab
cd http-server-lab
The directory you start the server from becomes its document root: the folder where it looks for requested files.
2. Create the home page
Create a file named index.html in http-server-lab and add:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Server lab</title>
</head>
<body>
<h1>Hello from my Python server</h1>
<p>This response came from index.html.</p>
<a href="/info.html">Open the information page</a>
</body>
</html>
index.html is special: when a browser requests a directory such as /, the server looks for this file and returns it automatically.
3. Create a second page
Beside it, create info.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Information</title>
</head>
<body>
<h1>Information</h1>
<p>This is a different file and therefore a different resource.</p>
<a href="/">Return home</a>
</body>
</html>
Your directory should now contain:
http-server-lab/
├── index.html
└── info.html4. Start the server
From inside http-server-lab, run:
python3 -m http.server --bind 127.0.0.1 8000
Read the command left to right:
python3 -m http.serverruns Python's built-in HTTP server module.--bind 127.0.0.1makes it reachable only from your own computer.8000is the port where it listens.
Leave this terminal open. The server is now inside its accept → parse → route → respond loop, waiting for a client.
5. Retrieve both pages in a browser
Open these addresses:
http://127.0.0.1:8000/retrievesindex.html.http://127.0.0.1:8000/info.htmlretrievesinfo.html.
Clicking the link on either page performs another HTTP request and moves between the two files. Watch the terminal while you browse. It will print one access-log entry per request, similar to:
127.0.0.1 - - [16/Aug/2026 10:30:00] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [16/Aug/2026 10:30:05] "GET /info.html HTTP/1.1" 200 -
The path after GET is what the browser requested; 200 means the server found and returned it.
Now visit http://127.0.0.1:8000/missing.html. There is no matching file, so the browser receives a 404 page and the terminal records 404. That is the routing fall-through from the simulator, happening for real.
6. Change a response
Keep the server running, edit the paragraph in info.html, save it, and refresh the information page. The change appears without restarting because this static server reads the file again when it handles the next request.
When you are finished, return to the terminal and press Ctrl+C to stop the server.
Static vs dynamic: read a file, or run code
Every response a server produces is one of two kinds, and the difference drives most of web architecture:
- Static — the response already exists as a file (
logo.png,style.css, this very page). The server's job is just read from disk and send bytes. Same answer for everyone, which makes static responses endlessly cacheable — by proxies, CDNs, and browsers. - Dynamic — the response is computed per request: run a handler, maybe query a database, build HTML or JSON on the spot. Your dashboard and my dashboard come from the same URL but different data.
This is why real deployments split the work: a fast static server or CDN handles files, and your application only wakes up for the dynamic part.
Surviving more than one client
Here's the problem the loop hides: while your handler is busy computing one response, new requests keep arriving. With a single worker, one slow request stops the whole site:
The two classic escapes:
- More workers — threads or processes, each running the same loop. A slow request occupies one worker; the others keep serving. This is how Gunicorn/uWSGI-style servers run Python apps.
- An event loop — one worker that never waits: whenever a request is blocked on the database or disk, it sets the work aside and serves someone else (Node.js, nginx, Python's
asyncio).
If this sounds familiar, it should — it's the browser's main-thread lesson from How browsers work, mirrored on the server: whoever holds the only thread must never hold it long.
In production the pieces compose: a reverse proxy (nginx, Caddy, a cloud load balancer) sits in front, serves static files, ends the TLS connection, and spreads dynamic requests across a pool of application workers. Every "how do I deploy my app" tutorial is some arrangement of exactly these boxes.
Takeaways
- A server is a loop: accept → parse → route → handler → respond. Frameworks decorate the loop; they don't replace it.
- Routing is a lookup table, and 404 is the table's fall-through working as designed.
- Static = read a file, dynamic = run code. Push static to CDNs/caches; spend your server on the dynamic part.
- One worker means one slow request blocks everyone — scale with worker pools or an event loop.
- Real deployments layer it: reverse proxy for static/TLS/balancing, application workers for the dynamic core.
References
- Fielding, R., Nottingham, M., & Reschke, J. (2022a). HTTP/1.1 (RFC 9112). Internet Engineering Task Force. https://doi.org/10.17487/RFC9112
- Fielding, R., Nottingham, M., & Reschke, J. (2022b). HTTP semantics (RFC 9110). Internet Engineering Task Force. https://doi.org/10.17487/RFC9110
- Kegel, D. (2006). The C10K problem. Retrieved August 16, 2026, from http://www.kegel.com/c10k.html
- MDN Web Docs. (n.d.). What is a web server? Mozilla. Retrieved August 16, 2026, from https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_web_server
- nginx. (n.d.). Inside NGINX: How we designed for performance & scale. Retrieved August 16, 2026, from https://www.f5.com/company/blog/nginx/inside-nginx-how-we-designed-for-performance-scale
- Python Software Foundation. (n.d.). http.server — HTTP servers. Retrieved August 16, 2026, from https://docs.python.org/3/library/http.server.html