From a Click to the Database: The Journey of One Web Request
One click travels through seven legs: address lookup, connection, security, the request, the application, the database and the reply. An explanation with a parcel picture, how to time each leg with curl, exercises with answer keys and the mix-ups to avoid when you ask AI to fix a slow page.
For beginners, for people who build products, and for anyone who asks AI to fix a slow web page without knowing where the problem sits. No networking background is needed. Every hard word comes with an everyday picture, and the calculation parts are marked so you can skip them.
Numbers and definitions come from public documentation listed at the end. Examples labeled "illustration" or "simulation" are reconstructed and name no real person or company. Real systems have more detail than this article, so the pictures below are only aids for remembering.
What I could not do, up front: the environment where I wrote this blocks opening local network ports and blocks outbound https. So I could not run curl against any server. The "real run" section is a simulation I wrote and really ran, but the delays are set by me, not measured on a network. The curl commands in the article are marked "not run here". Run them on your own machine to get real numbers.
Quick summary#
- When you open a page, the request passes many legs: address lookup, connection, security handshake, the request, the application, the database, the reply. Each leg can be slow or fail on its own.
- To find the slow leg, measure, do not guess. The
curlcommand with the-woption prints timing marks. - curl's marks are cumulative from the start. The time of one leg is a mark minus the mark just before it.
- In this article's simulation, two cases have similar totals (0.973 s and 1.383 s) but very different culprits: waiting for the database (about 83%) in one, DNS lookup (about 87%) in the other.
- A
200code only says the request succeeded at the protocol level. It does not say the content is right. - When you ask AI to fix a slow page, measure first and give it the numbers. AI-written code can hide many small queries that are hard to see, but I found no study measuring how often.
After this article you can explain#
- Which legs a web request has, and why each can be slow or fail by itself.
- Why curl's timing marks must be subtracted, and how to compute each leg.
- How to narrow a problem down to the network, the application or the database before asking AI to fix it.
Hard words, plain meaning#
| Term | Plain meaning |
|---|---|
| DNS (domain name system) | The phone book of the internet. You give it the name "example.com" and it returns the numeric address of the server. |
| TCP (connection) | A phone call saying "I am about to send goods". The other side picks up and confirms before anything is sent. |
| TLS (security handshake) | Both sides agree on a locked box before sending goods, so outsiders cannot look inside. Present in addresses that start with https. |
| HTTP | The language of order slips and reply slips between the browser and the server. |
| Header | Note lines attached to the slip: content type, language, length. |
| Status code | The short number on the reply slip: 200 is done, 404 is not found, 500 is a server error. |
| Database | The warehouse that stores information for the long term. The application must ask it to get data to answer with. |
| Latency | Waiting time, in seconds or milliseconds (one millisecond is a thousandth of a second). |
1. A picture first: sending a parcel#
Picture ordering goods from a shop in another city. Before the goods reach you, seven things happen:
- You look up the phone book for the shop's address (DNS).
- You call to say there is an order and wait for them to pick up (TCP connection).
- Both sides agree on a locked box (TLS).
- You send the order slip saying what you want (HTTP request).
- The front desk receives the slip and handles it (the application on the server).
- The front desk goes to the warehouse to find the goods (the database).
- The goods and a slip come back to you (the reply).
If the goods arrive late, you cannot tell at once where the problem is: an old phone book, a busy line, the front desk, or the warehouse. The most trustworthy person is the one who times each step. Computers are the same.
MDN describes it more briefly with a road picture: your device is the house, the server is the shop, DNS is the address book, and HTTP is the language you order in. MDN lists four steps: DNS lookup, request, response, assembly. This article splits it finer so you can see which parts are measurable.
Takeaway: "the page is slow" is not one fault. It is the sum of seven legs, and usually one leg takes most of the time.
2. The seven legs, one by one#
The table is a simplified map. The last column says how each leg tends to fail, so you can recognize it from an error message.
| # | Leg | Picture | Often slow when | Often fails as |
|---|---|---|---|---|
| 1 | DNS | Looking up the phone book | The DNS server is far or overloaded, a brand-new domain | The name cannot be resolved: the browser says the server was not found |
| 2 | TCP connection | Calling ahead | The server is far, the network drops packets | The connection is refused or times out |
| 3 | TLS | The locked box | A far network, several rounds of negotiation | An expired or wrongly named certificate: the browser warns it is not secure |
| 4 | Sending the request | The order slip | The data sent up is large | Missing permission (401, 403), a wrong header |
| 5 | Application | The front desk handling it | Slow code, big loops | A 500 error when the code meets a case it cannot handle |
| 6 | Database | Going to the warehouse | A missing index, too many small queries | Out of connections, queries that time out |
| 7 | Reply | Delivering the goods | Large content, not compressed | Cut off midway, missing content |
2.1 Leg 1: DNS, the phone book#
Picture it as: asking directory enquiries for the shop's number. You know the name, not the number.
Computers talk to each other with numeric addresses. The browser asks a DNS server to turn "example.com" into a numeric address. The result is usually remembered for a while (cached), so the next time is faster. That is why the first measurement is often slower than later ones.
2.2 Legs 2 and 3: connection and security handshake#
Picture it as: phoning ahead, then both sides agreeing on a locked box.
According to MDN, HTTP runs on TCP and is usually encrypted with TLS. HTTP needs a reliable channel, so it opens a connection first. For an https address there is an extra TLS handshake where both sides agree on a key. The old HTTP/1.0 opened a new connection for each request and reply, while later versions reuse the connection, so a second request to the same server often skips legs 1 to 3.
2.3 Leg 4: sending the HTTP request#
Picture it as: the order slip. It names the item (the path), the way of ordering (the method) and a few notes (headers).
MDN describes a request as a method (such as GET to fetch, POST to send), the path of the resource, the HTTP version, optional headers and possibly a body. An example following MDN's pattern:
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
A reply has the version, a status code with a few words, headers and possibly a body (the page content):
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 29769
The five status code classes according to MDN: 1xx informational, 2xx success, 3xx redirection, 4xx a fault on the side that sent the request, 5xx a fault on the server side. Some common codes: 200 success, 301 moved permanently to another address, 304 not changed so use the saved copy, 404 not found, 500 a general server error, 503 temporarily unable to serve.
2.4 Legs 5 and 6: the application and the database#
Picture it as: the front desk reads the slip, then goes to the warehouse. Every trip to the warehouse takes time there and back.
For a dynamic page, time often piles up in these two legs. The application runs your code, and if it needs data it asks the database. Each ask is a round trip, especially when the database sits on another machine. Asking 100 small times is usually slower than asking one big time, because you pay for 100 trips. Section 4.1 has a worked example.
2.5 Leg 7: the reply comes back#
The bigger the content, the longer it takes, especially if it is not compressed. For a text page this is usually the shortest leg, but it can be long for large images or downloads.
Takeaway: every leg has its own "failure signature". The error message is a clue worth reading carefully before asking AI to guess.
3. Timing each leg with curl#
Picture it as: putting a stopwatch at every door along the parcel's route.
curl is a command-line tool for sending HTTP requests. Two options matter here:
-v(verbose): prints the whole process. According to curl's documentation, a line starting with*is information from curl itself (name lookup, connection), a line starting with>is what curl sent, and a line starting with<is what curl received.-w(write-out): prints measurements after the transfer, in a template you set.
Five marks to know. All are in seconds and all count from the start, according to the libcurl documentation:
| Mark | Meaning |
|---|---|
time_namelookup |
DNS lookup finished |
time_connect |
TCP connection finished |
time_appconnect |
TLS handshake finished (the libcurl documentation does not say what the value is without TLS and I did not verify it myself, so for an http address ignore this mark) |
time_starttransfer |
First byte of the reply received, including the server's processing time |
time_total |
Everything finished |
A sample command (not run here, because my environment blocks outbound https; run it on your machine):
curl -s -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' https://example.com/
I did run a command of this form with curl 8.7.1 and curl accepted the variable names, but the connection was blocked, so the status code came out as 000 and the numbers are meaningless. I do not use them.
3.1 The subtraction formulas#
Because the marks are cumulative, the time of each leg is:
DNS = time_namelookup
TCP connection = time_connect - time_namelookup
TLS handshake = time_appconnect - time_connect
Server wait = time_starttransfer - time_appconnect
Download = time_total - time_starttransfer
For an http address (no TLS), drop the TLS line and use time_connect in place of time_appconnect on the "Server wait" line (an approximation).
An important limit: the "server wait" interval contains both the application (leg 5) and the database (leg 6). curl cannot separate them. To separate them you need query logs or a dedicated monitoring tool.
4. A real run: simulating the seven legs (the calculation can be skipped)#
This is a simulation I wrote in Python 3.14.8 and really ran in my environment. Each leg is a wait I set in advance. The program prints cumulative marks like curl does, then computes each leg by subtraction. The delays are numbers I chose, not measured from a network. What is being tested is whether the subtraction returns what I set.
Case A: a slow database (DNS 0.040 s, connect 0.030, TLS 0.060, server wait 0.800, download 0.020). Case B: slow DNS (DNS 1.200, connect 0.030, TLS 0.060, server wait 0.050, download 0.020).
The real output of the run:
A: slow database
time_namelookup 0.045 leg = 0.045
time_connect 0.080 leg = 0.035
time_appconnect 0.144 leg = 0.064
time_starttransfer 0.949 leg = 0.805
time_total 0.973 leg = 0.024
longest leg: time_starttransfer (0.805 s)
B: slow DNS, fast database
time_namelookup 1.205 leg = 1.205
time_connect 1.240 leg = 0.035
time_appconnect 1.305 leg = 0.065
time_starttransfer 1.360 leg = 0.055
time_total 1.383 leg = 0.023
longest leg: time_namelookup (1.205 s)
Reading the result:
- Case A: the server-wait leg is 0.805 s out of 0.973, about 83%. The 0.805 is a little above the 0.800 I set because a real wait always overshoots by a few milliseconds.
- Case B: the DNS leg is 1.205 s out of 1.383, about 87%. The totals of the two cases are close (under 1.4 s), but what needs fixing is completely different.
- If you only look at
time_total, you cannot tell the two cases apart. Fixing the database in case B would not help.
To be clear: this is a simulation with delays I set. It proves the method of calculation, not what a real network looks like. The numbers on your machine will differ.
4.1 Extra queries: a real count, an assumed time#
The second example also ran for real, in Python with SQLite 3.53.4 in memory. There are 50 customers and 200 orders. Way one: ask for the list of customers, then one separate query per customer. Way two: one query with a join and a group.
same answer: True
queries, one per row: 51
queries, one join : 1
waiting for the database at 5 ms per query: 255 ms vs 5 ms
Both ways give the same answer, but one costs 51 queries and the other costs 1. The counts are real. The 5 milliseconds per round trip is my assumption for a database on another machine (SQLite in memory has almost no network delay). Under that assumption, 51 queries wait 255 ms and 1 query waits 5 ms. This pattern is called N+1 (one query for the list, then N queries, one per row).
Takeaway: when the "server wait" leg is long, the first question is not "which query is slow" but "how many queries does this page send".
5. Exercises with answers#
These exercises use a fake server on your own machine. I could not run this server because my environment blocks opening network ports (the command that opens the port reports "Operation not permitted"). I could only check that the code compiles. The answers below follow from how I wrote the code, so run it for real and compare.
5.1 The fake server#
Save it as slow_app.py:
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/slow":
time.sleep(0.8) # simulate a slow query
body = b"orders: 3\n"
elif self.path == "/missing":
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
return
else:
body = b"hello\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
Run python3 slow_app.py in one terminal window and use another window for the commands below.
5.2 Exercise 1: find the slow leg#
curl -s -o /dev/null -w 'tcp=%{time_connect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://127.0.0.1:8765/slow
curl -s -o /dev/null -w 'tcp=%{time_connect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://127.0.0.1:8765/
Question: in each command, which leg takes almost all the time? Work it out by subtraction.
Answer (derived from the code, compare with your numbers): the first command has first_byte minus tcp of about 0.8 seconds (plus a few milliseconds of error), because the code waits 0.8 s before replying. The rest is close to zero since both ends are the same machine. The second command has no wait, so all the numbers are tiny. Because the address is http, with no TLS, you use time_connect in place of time_appconnect.
5.3 Exercise 2: a 404 and the timing#
curl -s -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://127.0.0.1:8765/missing
Answer: code=404 and a very small time. The lesson: an error reply can still be fast. A short time does not mean correct.
5.4 Exercise 3: one measurement is not enough#
Run the exercise 1 command (the /slow path) five times and write down the first_byte values. Take the middle value when sorted from smallest to largest (the median).
Answer: the numbers should be near 0.8 seconds but differ by a few milliseconds. On a real network the spread is much larger, so do not conclude from a single run.
5.5 Try it with https (not run here)#
curl -v https://example.com/ -o /dev/null
curl -s -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' https://example.com/
In the -v command, look for the * lines (connection and handshake), the > lines (the order slip curl sends) and the < lines (the reply slip). With the -w command, apply the formulas in section 3.1. The first run often has slower DNS, and the second is often faster thanks to caching.
6. Where people get mixed up#
Picture checking a parcel: knowing it arrived late is not enough, you must know at which door.
| Situation | Why it misleads | How to catch it |
|---|---|---|
| The page is slow so fix the database right away | DNS or TLS may be the long leg (like case B above) | Measure first, subtract the marks, look at the biggest leg |
Looking only at status 200 |
200 only says the request succeeded at the protocol level, and the body can still be empty or hold an error message the application wrote | Check the returned content, not just the code |
| The second run was fast so it counts as fixed | Caches (DNS, browser, server) speed up later runs. A 304 deliberately lets you use the saved copy | Measure both the first and later runs and say which is which |
Using time_total as the time of one leg |
The marks are cumulative | Always subtract the earlier mark from the later one |
| A single measurement | Networks and servers fluctuate | Measure several times and take the median |
| There is a redirect (3xx) | Each extra request followed costs all the legs again. Per the libcurl documentation, times of followed requests are added together | Look at each request with -v, avoid needless redirects |
| Measuring from your machine and inferring for users | Users are elsewhere on other networks | Measure from several places if you can and say where |
7. Illustration: one morning with a slow report page#
This is an illustration, naming no real person or product. The names, counts and times are reconstructed.
- 09:30. A user reports the report page takes about 3 seconds to appear.
- 09:40. The dev asks an agent: "Make the query faster." The agent adds an index and says "optimized".
- 10:05. The page is still slow. The dev runs curl with
-wand seestcp=0.02,first_byte=3.05,total=3.08. Server wait takes nearly 3 seconds. DNS, connection and download are negligible. - 10:15. The dev turns on the query log: one page load sends 601 queries (1 query for the list and 600 queries, one per row). Each query waits about 5 milliseconds because the database is on another machine. The arithmetic: 601 times 5 milliseconds is 3,005 milliseconds, which matches about 3 seconds.
- 10:50. The dev asks the agent to merge them into one query with a join, and to print the query count before and after.
first_bytedrops to about 0.1 seconds (an assumed number in this example).
The action to take: measure first, then give the numbers to the AI. If you only say "make it faster", the AI can fix the wrong place, such as adding an index for a query that was never the culprit, and still report done. Ask for evidence as in the post Make AI Output Prove Itself: the query count and the first_byte before and after.
This illustration proves nothing about frequency. The evidence is elsewhere: section 4.1 really counted 51 queries versus 1 for the same answer. As for whether AI-written code often causes N+1, I found no study that measures it directly. A 2025 study (Unveiling Inefficiencies in LLM-Generated Code) analyzed 492 small code snippets produced by three open-source models on HumanEval+ and found logic and performance problems among the common inefficiencies, but those are small algorithm tasks, not web applications with a database. Treat N+1 as a possible risk and measure for yourself, not as a certainty. For the security of AI-written code, see also OWASP Top 10 for AI Apps.
8. Numbers and documents used#
| Source | What it says | How to read it | Caveat |
|---|---|---|---|
| MDN, How the web works | Four steps: DNS lookup, request, response, assembly | The most basic map | Very simplified, no mention of TLS |
| MDN, HTTP overview | Request and response model, stateless, runs on TCP (usually with TLS), reuses connections | Basis for legs 2, 3, 4 | A reference, no measurements |
| MDN, status codes | The five classes 1xx to 5xx and the meaning of 200, 301, 304, 404, 500, 503 | Basis for the status code part | Meanings at the protocol level |
| libcurl, STARTTRANSFER_TIME and APPCONNECT_TIME | Times count from the start, include server processing, add up across redirects | Basis for the subtraction formulas | The documentation does not say the value without TLS |
| Everything curl, verbose | Meaning of *, >, < |
Basis for reading -v |
A how-to guide |
| My simulation | How to compute each leg by subtraction | Illustrates the method | Delays set by me, not a network measurement |
| My query count | 51 queries versus 1 for the same answer | Illustrates N+1 | 5 ms per round trip is an assumption |
| A 2025 study of inefficiencies in model-generated code | Logic and performance problems are common across 492 snippets | A hint to measure, not a conclusion | Small algorithm code, three open-source models, not web applications |
Read it correctly: the documents support the way of splitting into legs and the way of computing. I did not measure on a real network and found no study measuring N+1 in AI-written code.
9. Checklist: eight questions when a page is slow#
- Did I measure with numbers, or am I guessing?
- Did I take the marks and subtract them, or am I using
time_total? - Which leg is biggest: DNS, connection, TLS, server wait or download?
- Was this the first run or a later one (with caches)? Did I measure several times?
- What is the status code, and is the returned content right?
- If the "server wait" leg is long, how many queries does this page send?
- When asking AI to fix it, did I give it the measurements and ask for measurements after the fix?
- Where did I measure from, and are the users somewhere similar?
10. A balanced view: what this map simplifies#
What it does: gives you a way to split the problem, a set of measuring formulas and a habit of checking before asking AI.
What it does not do:
- This is a simplified map of HTTP/1.1 over TCP with TLS. In practice there are also CDNs (content delivery networks), proxies and load balancers. Each adds a leg or removes one.
- HTTP/3, per RFC 9114, runs on QUIC, a protocol built on UDP, and merges the connection part with the security handshake. According to the secondary sources I read, this reduces the round trips when opening a connection. In that case
time_connectandtime_appconnectmay no longer separate as cleanly as the table in section 3. Read the RFC itself if you need precision. - curl measures from one place, one time. A single measurement is noise. Dedicated monitoring tools (APM) see more, such as separating the application from the database.
- The simulation in section 4 only illustrates the arithmetic, and I did not run the exercises in section 5 for real.
Do not overstate it: knowing seven legs does not make you an infrastructure expert, but it is enough to ask the right question. Do not wave it off: fixing the wrong place still costs time, and someone will still report "done".
11. Conclusion#
"The page is slow" is the sum of several legs, and usually one leg takes nearly all of it. Measure with curl -w, subtract each mark from the next and look at the biggest leg. If it is the server wait, count the queries before guessing. And when you ask AI to fix it, give it the measurements, ask for measurements after the fix, then open those numbers yourself.
References#
Definitions come from public documentation as of October 2026. The HTTP/3 part rests on secondary sources, so open the originals to check before quoting.
- MDN, How the web works
- MDN, HTTP overview and HTTP response status codes
- curl, CURLINFO_STARTTRANSFER_TIME and CURLINFO_APPCONNECT_TIME
- Everything curl, Verbose and the curl man page (I could not read the -w part of this page, the variable names were confirmed by running curl 8.7.1)
- IETF, RFC 9114 (HTTP/3) and RFC 9000 (QUIC)
- Unveiling Inefficiencies in LLM-Generated Code: Toward a Comprehensive Taxonomy, arXiv 2503.06327 (2025)
Frequently asked questions
- What legs does a web request pass through?
- In the simplified picture used here: look up the server address with DNS, open a TCP connection, do the TLS security handshake (for https), send the HTTP request, let the application work, let the application ask the database, then send the reply back. MDN describes it briefly in four steps: DNS lookup, request, response, assembly. Real systems can add CDNs, proxies or HTTP/3, so treat this as a map for orientation.
- How do I tell whether a page is slow because of the network, the application or the database?
- Run curl with -w to get time_namelookup, time_connect, time_appconnect, time_starttransfer and time_total, then subtract each mark from the next. The biggest leg shows where to look. The wait between time_appconnect and time_starttransfer includes both the application and the database, and curl cannot separate the two. You need query logs or a monitoring tool for that.
- Why do curl's timing marks have to be subtracted?
- Because they are cumulative from the start. The libcurl documentation describes time_starttransfer as the time from the start until the first byte of the response arrives, which already includes the earlier legs and the server's processing time. Reading time_total as the time of one leg is a misreading.
- Does a 200 status code mean everything is right?
- No. According to MDN, 200 means the request succeeded at the protocol level. The body can still be empty or can carry an error message written by the application, so check the content as well as the status code.
- Does AI-written code often make extra database queries?
- It can, but I did not find a study that measures the N+1 pattern (one query for a list, then one query per row) in AI-written code. A 2025 study of 492 small code snippets from three open-source models found logic and performance problems among the common inefficiencies, but that was algorithmic code, not web applications. The reliable way is to count the queries of your own page.