What happens when you open an app or website
Between a tap on the screen and the data appearing, your request makes several trips across the network. This lesson follows one request step by step, so you can see where the time goes and which steps you can remove.
By the end you can
- Name the steps of a request in order: DNS, TCP, TLS, HTTP request, server work, response
- Explain how DNS finds a server and what a DNS TTL does
- Read the parts of a raw HTTP request and response
- Explain why HTTP is stateless and how tokens and cookies carry identity
- Measure each step of a real request with
curl
The everyday picture
You want to order dinner from a restaurant you have never called. First you find the phone number. You search for the name and get a number. Next you dial and wait for someone to pick up. Then you check it is really the right restaurant, because you are about to give your address. Then you place the order in the way they expect: dish, quantity, address. Finally the food arrives.
Opening an app works the same way. Finding the number is DNS. Dialling and waiting for "hello" is TCP. Checking it is really them is TLS. Placing the order in the agreed format is the HTTP request. The food is the HTTP response. Every step costs time, and on a bad phone line each step takes longer.
The whole journey in one picture
The app on your phone is the client. The program that answers is the server. Between them, every step that needs an answer from the other side costs one round trip (RTT). On office Wi-Fi a round trip to a nearby data centre can take a few milliseconds. On weak mobile data in a busy market it can take hundreds.
Count the arrows that cross the whole width. Before the app sends its first useful byte, it has already spent around three round trips. If one round trip takes 150 ms, that is roughly 450 ms of waiting with nothing to show. Most of those steps happen only once, though. The next request on the same connection skips steps 1 to 3.
Step 1: DNS turns a name into an address
Computers on the internet find each other by IP address, not by name. DNS is the phone book that turns api.example.com into an address like 203.0.113.10.
The lookup goes through a chain, and every level keeps a copy of answers it has seen. First the app or browser checks its own memory. Then the phone's operating system checks its cache. If both miss, the phone asks a resolver, usually run by the internet provider or a public service. If the resolver has no copy either, it walks the chain itself: the root servers point it to the .com servers, and those point it to the domain's own name server, which gives the final answer.
The domain owner sets a TTL on each DNS record, for example 300 seconds. That number is a trade-off. A long TTL means fewer lookups and faster first requests. A short TTL means that when you move your API to a new server, clients find the new address sooner. Before a planned move, teams lower the TTL a day ahead, make the change, then raise it again.
A full lookup with empty caches can take tens to hundreds of milliseconds. A cached one takes almost nothing. This is one reason the first open of an app after a long time feels slower than the second.
Quick check
You used an app two minutes ago, and its API's DNS record has a TTL of 300 seconds. You open it again. Where does the DNS lookup most likely end?
The answer from two minutes ago is still inside its 300-second TTL, so a cache close to you answers almost at once. The root and the name server are asked only when every cache on the way has missed.
Steps 2 and 3: TCP connects, TLS proves identity
With an address in hand, the phone opens a TCP connection. It sends a "can we talk?" message (SYN), the server replies "yes" (SYN-ACK), and the phone confirms (ACK). That is the 3-way handshake. Networking you actually need looks at it in more detail.
Next comes TLS, the S in HTTPS. The server sends its certificate, which proves it really owns api.example.com. The two sides agree on encryption keys. From now on, nobody on the café's Wi-Fi or the mobile network can read or change the data. TLS 1.3 needs one round trip for this. The older TLS 1.2 needs two.
Here is the most useful fact in this section: a connection can be reused. HTTP/1.1 keeps connections open by default (called keep-alive), and HTTP/2 can send many requests at once over one connection. A reused connection skips DNS, TCP and TLS completely. The server decides how long an idle connection stays open. Node's HTTP server, which NestJS runs on, keeps it for 5 seconds by default (in Node 22; check your version), and you will see that in a header below.
Quick check
Which request can skip DNS, TCP and TLS completely?
The setup steps belong to the connection, not to the request. Once a connection is open, every request on it reuses that work. HTTPS makes setup longer, and the method or body size has nothing to do with it.
Steps 4 to 6: the HTTP request and response
HTTP is the agreed format for the order. In HTTP/1.1 it is plain text you can read. HTTP/2 and HTTP/3 send the same parts in a compact binary form, but the parts are the same.
The request has four parts:
- The method says what kind of action:
GETreads,POSTcreates,PUTorPATCHchanges,DELETEremoves. The path says which thing:/ordersor/sites/12/stock/4711. - Headers are extra facts as
Name: valuelines.Hostsays which website, because one server can host many.Authorizationsays who is asking.Content-Typesays how the body is written. - An empty line marks where the headers end.
- The body carries data. A
GETusually has none. APOSTcarries the new order as JSON.
The response has the same shape. Its first line has a status code: 200 means OK, 201 means created, 404 means not found, 500 means the server broke. APIs done right covers which codes to use when.
A website and a mobile app use HTTP the same way. The difference is what happens after the response. A browser gets HTML, then makes more requests for CSS, JavaScript and images, then draws the page. A mobile app such as the Mangrove app usually gets JSON and draws native screens from it. It skips the HTML, but it pays the same DNS, TCP, TLS and round-trip costs.
Quick check
A response starts with HTTP/1.1 404 Not Found. What does the app know before it reads the body?
The status line says what happened. 404 means the path names something that is not there. A crash would be a 5xx, and a missing or expired login is 401.
HTTP forgets you after every request
HTTP is stateless. The server does not remember that this phone logged in a minute ago. So every request must carry its own identity. Web apps usually send a cookie. Mobile apps usually send a token in a header: Authorization: Bearer eyJ…. The server checks it on every request.
This sounds wasteful, but it is a gift for scaling. Since no server remembers anything about you, any copy of the server can answer your next request. That is what lets you run three API servers behind a load balancer later (Scaling up vs scaling out). How to make and check tokens safely is in Security essentials.
Think first: a user opens the app, waits 3 seconds, then taps "refresh". Why is the refresh much faster than the first load?
On the first load, the app paid for a DNS lookup, a TCP handshake and a TLS handshake before it could send the request. On the refresh, the DNS answer is cached and the connection is probably still open, because the 5-second keep-alive has not run out. So the refresh pays only one round trip plus the server's work.
If the user waits 10 seconds instead, the server has likely closed the idle connection, and the app pays for TCP and TLS again (DNS is probably still cached).
Build it
We will run a tiny NestJS API, read its raw HTTP, then measure each step of a real request.
-
A minimal NestJS controller that returns JSON
Add this to a new Nest project (
npx @nestjs/cli new stock-api) and register the controller inAppModule. It uses a fake array, so you need no database yet. In Mangrove's data asiteis a store or a DC, and anarticleis a product, so this endpoint returns the stock of one product at one store.TypeScript// src/stock.controller.ts import { Controller, Get, NotFoundException, Param, ParseIntPipe } from '@nestjs/common'; type StockDto = { siteId: number; articleId: number; qty: number; updatedAt: string }; const fakeStock: StockDto[] = [ { siteId: 12, articleId: 4711, qty: 42, updatedAt: '2026-10-06T09:15:02.118Z' }, ]; @Controller('sites/:siteId/stock') export class StockController { // GET /sites/12/stock/4711 @Get(':articleId') findOne( @Param('siteId', ParseIntPipe) siteId: number, // "12" becomes 12, "abc" gives 400 @Param('articleId', ParseIntPipe) articleId: number, ): StockDto { const row = fakeStock.find((s) => s.siteId === siteId && s.articleId === articleId); if (!row) throw new NotFoundException(`No stock row for article ${articleId} at site ${siteId}`); return row; // Nest turns the object into JSON and sets status 200 } }Terminalnpm run start:dev # listens on http://localhost:3000 -
See the request and response with
curl -vThe
-vflag prints what curl sends (lines starting with>) and what it receives (<).Terminalcurl -v http://localhost:3000/sites/12/stock/4711Output* Trying 127.0.0.1:3000... * Connected to localhost (127.0.0.1) port 3000 <- TCP done > GET /sites/12/stock/4711 HTTP/1.1 <- request line > Host: localhost:3000 <- headers > User-Agent: curl/8.5.0 > Accept: */* > <- blank line < HTTP/1.1 200 OK <- status line < Content-Type: application/json; charset=utf-8 < Content-Length: 78 < Connection: keep-alive < Keep-Alive: timeout=5 <- idle connection kept 5 s < {"siteId":12,"articleId":4711,"qty":42,"updatedAt":"2026-10-06T09:15:02.118Z"}Output trimmed a little; you will also see
X-Powered-By,ETagandDateheaders. Try/sites/12/stock/999to get a 404 with a JSON error body, and/sites/abc/stock/1to get a 400 fromParseIntPipe. -
Send HTTP by hand
To prove HTTP/1.1 is only text, type the request yourself with
nc(netcat). The\r\npairs are the line endings HTTP requires, and the final empty line ends the headers.Terminalprintf 'GET /sites/12/stock/4711 HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\n\r\n' \ | nc localhost 3000You get back the same status line, headers and JSON body. A phone, a browser and curl all send text like this. With HTTPS, the same text travels inside the TLS encryption.
-
Look up a name with
digReplace
api.example.comwith your own API's domain.+noall +answershows only the answer. The second column is the TTL in seconds.Terminaldig +noall +answer api.example.com dig +trace api.example.com # walk the chain yourself: root, .com, name serverOutputapi.example.com. 300 IN A 203.0.113.10Run the first command again a minute later. If your resolver cached the answer, the TTL counts down (for example to 241), because the resolver is telling you how long its copy stays valid.
digcomes in thednsutilsorbind-utilspackage on Linux. -
Time every step with
curl -wSave this format file. Each variable is a time in seconds, counted from the start of the request, not from the previous step.
Outputdns lookup: %{time_namelookup}s\n tcp connect: %{time_connect}s\n tls handshake: %{time_appconnect}s\n first byte: %{time_starttransfer}s\n total: %{time_total}s\n status: %{http_code}\nTerminal# save the lines above as curl-timing.txt, then: curl -s -o /dev/null -w "@curl-timing.txt" https://api.example.com/healthOutputdns lookup: 0.152s tcp connect: 0.311s tls handshake: 0.498s first byte: 0.874s total: 0.951s status: 200Read it by subtracting each line from the next (these numbers are an example from a slow link):
- DNS: 152 ms.
- TCP: 311 − 152 = 159 ms, which is about one round trip.
- TLS: 498 − 311 = 187 ms, about one more round trip (TLS 1.3).
- Waiting for the first byte: 874 − 498 = 376 ms. This is one round trip plus server work, so the server took roughly 376 − 160 ≈ 216 ms.
- Download: 951 − 874 = 77 ms.
-
Watch connection reuse
Give curl the same URL twice in one command. It reuses the open connection for the second request.
Terminalcurl -s -o /dev/null -o /dev/null -w "@curl-timing.txt" \ https://api.example.com/health https://api.example.com/healthFor the second request,
tcp connectandtls handshakeshow0.000000s, and the total drops to about one round trip plus server time. That difference is what keep-alive saves on every request after the first.Steps 1 to 6 need a terminal on your own machine. The subtraction from step 5, which is where most people slip, is also in the Practice section as a JavaScript exercise you can run in your browser.
Bring it to your team
Ask these when a screen feels slow, before anyone starts optimising the database.
- "Have we measured this with curl timing or a network trace from a real store or a real phone, not only from the office?"
- "How many requests does this screen make before it shows anything, and do they run one after another?"
- "Do we reuse connections, or does each call open a new one?"
- "How much of the time is the server working, and how much is the network?"
- "What is the TTL on our API's DNS record, and who changes it before a server move?"
- "How big is the response for this screen, and is it compressed?"
Red flags
- A screen makes five or more API calls in a row before it can render.
- Speed is only tested on office Wi-Fi next to the server.
- A client library creates a new HTTP client, and so a new connection, for every request.
- A server move is planned while the DNS TTL is still set to a day or more.
Practice
Turn curl's timings into steps
curl's -w timings are all counted from the start of the request. Write phaseTimes(t), which takes those timings in seconds and returns how long each step took, in whole milliseconds (use Math.round): dns, tcp, tls, wait (from the end of setup to the first byte) and download. One detail: for a plain http:// address there is no TLS, and curl prints 0 for appconnect. Then the TLS step is 0 and the wait starts when TCP finished.
// t = { namelookup, connect, appconnect, starttransfer, total } in seconds
function phaseTimes(t) {
const dns = 0;
const tcp = 0;
const tls = 0;
const wait = 0;
const download = 0;
return { dns, tcp, tls, wait, download };
}
test('reads the slow-link example from Build it', () => {
expect(phaseTimes({ namelookup: 0.152, connect: 0.311, appconnect: 0.498, starttransfer: 0.874, total: 0.951 }))
.toEqual({ dns: 152, tcp: 159, tls: 187, wait: 376, download: 77 });
});
test('TLS is the gap after TCP, not the total so far', () => {
expect(phaseTimes({ namelookup: 0.01, connect: 0.30, appconnect: 0.48, starttransfer: 0.60, total: 0.62 }).tls).toBe(180);
});
test('plain http:// has no TLS step, so the wait starts after TCP', () => {
expect(phaseTimes({ namelookup: 0.004, connect: 0.012, appconnect: 0, starttransfer: 0.150, total: 0.155 }))
.toEqual({ dns: 4, tcp: 8, tls: 0, wait: 138, download: 5 });
});
Show a solution
function phaseTimes(t) {
const ms = (seconds) => Math.round(seconds * 1000);
const setupDone = t.appconnect > 0 ? t.appconnect : t.connect; // no TLS: setup ends after TCP
return {
dns: ms(t.namelookup),
tcp: ms(t.connect - t.namelookup),
tls: t.appconnect > 0 ? ms(t.appconnect - t.connect) : 0,
wait: ms(t.starttransfer - setupDone),
download: ms(t.total - t.starttransfer),
};
}Plan a faster home screen
The Partner app's home screen takes about 3 seconds in Ravi's corner shop, which runs on mobile data. Here are the facts. One round trip from the shop is about 200 ms. The server works about 100 ms per call. The screen makes 4 calls, one after another. The app's HTTP client opens a new connection for every call (TCP, then TLS 1.3), while DNS stays cached after the first lookup. Count each of DNS, TCP and TLS as one round trip. Work out the current time, then the time after each of two fixes, and say which fix you would do first and how you would prove it helped.
- You get about 3 seconds today: one DNS lookup, then 4 calls that each pay TCP, TLS, the request and the server's work.
- You show that reusing one connection removes two round trips from every call after the first.
- You show that running the calls in parallel, or combining them into one endpoint, removes most of what is left.
- You do not start with database tuning, because the server's work is only 400 ms of the total.
- You say how you would measure before and after, for example curl timings or a network trace from a real phone on mobile data.
Show a model answer
Today: DNS 200 ms once, then each call pays TCP 200 + TLS 200 + request 200 + server 100 = 700 ms. 200 + 4 × 700 = 3,000 ms.
Fix 1, reuse the connection: DNS 200 + TCP 200 + TLS 200 once, then 4 × (200 + 100) = 1,200. Total 1,800 ms. Fix 2, parallel or one combined endpoint, on top of fix 1: 600 ms of setup plus one round of 300 ms = about 900 ms.
Do fix 1 first: it is usually a one-line change (share one HTTP client in the app) and cuts 1.2 seconds. Then combine or parallelise the calls. Prove it with the same measurement before and after, taken on a real phone on mobile data, not on office Wi-Fi.
Check yourself
curl shows time_connect 0.30 s and time_appconnect 0.48 s. How long did the TLS handshake take?
curl's timings are all measured from the start of the request. TLS starts when TCP finishes (0.30 s) and ends at 0.48 s, so it took 0.18 s. Reading 0.48 s as the TLS time is the common mistake: it includes DNS and TCP too.
Your team moves the API to a new server, but some stores keep reaching the old one for hours. What is the most likely cause?
Cached DNS answers are reused until their TTL expires, so a long TTL keeps old addresses alive. A certificate problem would show errors, not traffic to the old server. Keep-alive matters only for seconds to minutes, not hours, on a typical server.
Why can any of three identical API servers answer a user's next request?
Each request includes everything the server needs, including who you are, so no server has to remember earlier requests. TCP connections do not move between servers, and TLS only encrypts the connection; it does not share application sessions.
Flashcards
Key takeaways
- A first request goes DNS, TCP, TLS, HTTP request, server work, response. DNS, TCP and TLS each cost about one round trip.
- DNS answers are cached at many levels for the TTL. Lower the TTL before moving servers.
- HTTP messages have a first line, headers, an empty line and a body. The status code says what happened.
- HTTP is stateless. Tokens and cookies carry identity, which lets any server copy answer.
- On slow networks, round trips cost more than server work. Reuse connections and make fewer calls in a row.
Sources
- RFC 9110: HTTP Semantics, IETF, 2022: methods, status codes and headers
- RFC 1034: Domain Names, Concepts and Facilities, IETF, 1987: how DNS and its caching work
- RFC 8446: TLS 1.3, IETF, 2018: the one-round-trip handshake
- Write out, in Everything curl: every
-wtiming variable