---
title: "From a Click to the Database: The Journey of One Web Request"
url: https://www.stacklogs.io.vn/en/engineering/journey-of-one-web-request/
language: en
author: "Nhu Duy Thin (Thinnd)"
published: 2026-10-30
updated: 2026-10-30
section: "Engineering"
tags: ["HTTP", "DNS", "TLS", "curl", "database", "performance", "beginners"]
summary: "A web request passes seven legs: address lookup (DNS), connection (TCP), security handshake (TLS), the HTTP request, the application, the database and the reply. The timing marks of curl -w are cumulative from the start, so the time of one leg is its mark minus the mark just before it."
license: "Cite with the author name and the canonical URL above."
---

# From a Click to the Database: The Journey of One Web Request

> 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 `curl` command with the `-w` option 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 `200` code 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

1. Which legs a web request has, and why each can be slow or fail by itself.
2. Why curl's timing marks must be subtracted, and how to compute each leg.
3. 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:

1. You **look up the phone book** for the shop's address (DNS).
2. You **call to say** there is an order and wait for them to pick up (TCP connection).
3. Both sides agree on a **locked box** (TLS).
4. You **send the order slip** saying what you want (HTTP request).
5. The **front desk** receives the slip and handles it (the application on the server).
6. The front desk **goes to the warehouse** to find the goods (the database).
7. 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:

```http
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
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):

```bash
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:

```text
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:

```text
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.

```text
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`:

```python
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

```bash
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

```bash
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)

```bash
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 `-w` and sees `tcp=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_byte` drops 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](https://www.stacklogs.io.vn/en/engineering/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](https://www.stacklogs.io.vn/en/engineering/owasp-top-10-for-ai-apps-everyday-examples/).

---

## 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

1. Did I measure with numbers, or am I guessing?
2. Did I take the marks and subtract them, or am I using `time_total`?
3. Which leg is biggest: DNS, connection, TLS, server wait or download?
4. Was this the first run or a later one (with caches)? Did I measure several times?
5. What is the status code, and is the returned content right?
6. If the "server wait" leg is long, how many queries does this page send?
7. When asking AI to fix it, did I give it the measurements and ask for measurements after the fix?
8. 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_connect` and `time_appconnect` may 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](https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Web_standards/How_the_web_works)
- MDN, [HTTP overview](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview) and [HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status)
- curl, [CURLINFO_STARTTRANSFER_TIME](https://curl.se/libcurl/c/CURLINFO_STARTTRANSFER_TIME.html) and [CURLINFO_APPCONNECT_TIME](https://curl.se/libcurl/c/CURLINFO_APPCONNECT_TIME.html)
- Everything curl, [Verbose](https://everything.curl.dev/usingcurl/verbose/index.html) and the [curl man page](https://curl.se/docs/manpage.html) (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)](https://www.rfc-editor.org/rfc/rfc9114.html) and [RFC 9000 (QUIC)](https://www.rfc-editor.org/rfc/rfc9000.html)
- Unveiling Inefficiencies in LLM-Generated Code: Toward a Comprehensive Taxonomy, [arXiv 2503.06327](https://arxiv.org/abs/2503.06327) (2025)
