Phase 1 · Chapter 2
The Journey of a Request
DNS, TCP, TLS, HTTP — from browser to your Node.js code, every step
What this chapter does to you: A student opens attemptiq.com. You will be able to narrate, in exact physical detail, every single thing that happens before your Node.js server sees the first byte of their request. Most engineers can't do this. You will be able to do it in a system design interview without blinking.
The Big Picture First
Before we zoom in, here is the complete journey — every layer:
Five layers. Each layer sits on top of the previous one. Each one adds something the layer below doesn't provide.
This is called the network stack. When someone says "it's a networking issue" — they usually can't tell you which layer. You will be able to.
Part 1: DNS — The Internet's Phone Book
The Problem
Computers talk to each other using IP addresses — numbers like 138.68.74.52. That's your Hetzner server's address. But humans can't remember 138.68.74.52. We remember attemptiq.com.
DNS (Domain Name System) is the system that translates one to the other. Every single time.
The Physical Journey of a DNS Query
When a student's browser needs to resolve attemptiq.com, here's what happens — in order, no skipping:
Step 1: Browser cache
The browser first checks its own DNS cache. If it resolved attemptiq.com recently and the result hasn't expired, it uses that. Done. No network needed.
Step 2: OS cache
If the browser doesn't have it, the OS checks its own DNS cache. On Linux/Mac there's also /etc/hosts — a file you can manually add entries to. (This is how developers fake domains locally: 127.0.0.1 api.myapp.local.)
Step 3: Recursive Resolver
If neither cache has it, the OS sends a DNS query to a recursive resolver. This is usually your ISP's DNS server, or if you've configured it manually, something like Cloudflare's 1.1.1.1 or Google's 8.8.8.8.
This resolver is called "recursive" because it will do all the remaining work on your behalf and come back with a final answer.
The DNS query itself is sent over UDP, port 53.
Why UDP? Remember from Chapter 1 — UDP has no handshake overhead. A DNS query is tiny (maybe 50 bytes). It's fast to just fire it and wait. If it gets lost, the resolver retries. The tradeoff is fine.
Step 4: Root Nameserver
If the recursive resolver doesn't have the answer cached, it asks a root nameserver.
There are exactly 13 root nameserver clusters in the world, labeled A through M. They don't know attemptiq.com's IP — but they know where to find the nameservers responsible for the .com top-level domain.
Root says: "I don't know attemptiq.com, but here are the nameservers for .com."
Step 5: TLD Nameserver
The recursive resolver now asks the .com TLD (Top-Level Domain) nameserver.
The TLD doesn't know the final IP either, but it knows which nameserver is authoritative for attemptiq.com specifically. Because when you registered attemptiq.com and pointed it to Cloudflare DNS, that's what got registered here.
TLD says: "I don't know the IP, but kate.ns.cloudflare.com does."
Step 6: Authoritative Nameserver (Cloudflare DNS)
The recursive resolver asks Cloudflare's nameserver.
This one actually knows. It has the DNS record you configured: attemptiq.com → A → 138.68.74.52.
It responds with the IP and a TTL (Time To Live) — a number in seconds saying "cache this answer for this long."
Step 7: Answer propagates back
The recursive resolver caches the answer, then sends it back to the OS. The OS caches it. The browser caches it. Finally, the browser has the IP.
The vocabulary:
A Record= maps a domain to an IPv4 addressAAAA Record= maps a domain to an IPv6 addressCNAME Record= maps a domain to another domain (alias)TTL= how long resolvers should cache this answerAuthoritative Nameserver= the one that actually owns the recordRecursive Resolver= does the work of walking the hierarchy for you
Why TTL is Dangerous When You're Migrating Servers
Say you're moving AttemptIQ from one Hetzner server to another. You update the DNS record to point to the new IP. But if your TTL is 86400 (24 hours), then every recursive resolver in the world that already cached the old IP will keep serving the old answer for up to 24 hours.
This is why you lower your TTL to 300 (5 minutes) before a migration, wait for caches to expire, then change the IP, then raise the TTL back. Most engineers learn this the hard way at 2am.
Part 2: TCP — Why the Connection is Reliable
The Problem TCP Solves
DNS gave us an IP address. Now we need to actually send data to that IP.
The internet layer below TCP is called IP (Internet Protocol). IP does one thing: route packets from a source IP to a destination IP, hopping through routers across the world.
But IP is unreliable by design. A packet can be:
- Lost — a router dropped it because its queue was full
- Duplicated — the same packet arrives twice
- Reordered — packet 5 arrives before packet 3
- Corrupted — some bits flipped in transit
IP doesn't care. It's a best-effort delivery service. Like dropping letters in a postbox and hoping they arrive.
TCP sits on top of IP and adds reliability. TCP's job is to make a fundamentally unreliable channel behave as if it's perfectly reliable, ordered, and error-free.
It does this with four mechanisms. Understand all four.
Mechanism 1: The Three-Way Handshake (Connection Establishment)
Before a single byte of your HTTP request is sent, TCP establishes a connection between your browser and the server. This takes exactly one round trip.
SYN = synchronize. Setting the starting sequence number. ACK = acknowledge. Confirming what was received. Sequence number = a counter on every byte sent. Used to detect loss, reordering, and duplicates.
This handshake costs one RTT (Round Trip Time) before any data flows. If you're in India and your server is in Germany, RTT is ~150ms. Before HTTPS even starts, 150ms is already spent just establishing the TCP connection. This is why server location matters.
Mechanism 2: Sequence Numbers and Acknowledgments
Every byte TCP sends is numbered. The receiver sends back ACKs saying which byte it's received up to.
This is called retransmission. It's what makes TCP reliable.
The hidden cost: If you're streaming video and one TCP packet is lost, the entire stream must wait for that packet to be retransmitted. Even if later packets already arrived. This is called head-of-line blocking and it's why video calls don't use TCP.
Mechanism 3: Flow Control
The receiver has a limited buffer (RAM) for incoming data. If the sender blasts data faster than the receiver can process it, the buffer overflows and packets are dropped.
TCP solves this with a receive window (rwnd) — the receiver tells the sender "I have space for N bytes right now." The sender never sends more than that.
Your Postgres connection pool does something similar at the application level — it limits how many queries can be in-flight simultaneously so the DB isn't overwhelmed.
Mechanism 4: Congestion Control
The network itself has limits. Too much data causes router queues to fill up and packets to drop. TCP detects this and automatically slows down.
It starts with Slow Start — send a small amount, double it each round trip. When a packet drops, halve the rate. This probing behavior is why your download speed ramps up rather than immediately hitting max speed.
The vocabulary:
RTT= Round Trip Time. The time for a packet to go and an ACK to come back.SYN / ACK / FIN= TCP control flags. Synchronize, Acknowledge, Finish.Sequence number= per-byte counter. The foundation of all reliability.Retransmission= resending lost packets. Reliable but adds latency.Flow control= receiver controls sender speed via window size.Congestion control= TCP controls speed based on network health.Head-of-line blocking= later packets stuck waiting for a lost earlier one.Connection pooling= reusing established TCP connections instead of handshaking every time.
Why Your Postgres Connection Pool Exists
Every new Pool() in your pg client creates a fixed number of TCP connections to Postgres — and keeps them alive. When your code runs a query, it borrows an existing connection from the pool rather than doing a new three-way handshake.
Without pooling: every query = 1 TCP handshake = 1 RTT of pure overhead before any SQL is sent. At 100 queries/second, you're doing 100 handshakes/second. At the network level this is insane.
With pooling: connections stay open. You pay the handshake cost once per connection, not once per query. The query goes straight through.
This is not just a performance trick. It's the direct application of understanding TCP's cost.
Part 3: TLS — Why HTTPS Doesn't Destroy Performance
The Two Problems TLS Solves
TCP gives you a reliable channel. But it's a transparent pipe — anyone who can intercept traffic between your browser and server can read everything. HTTP over plain TCP means passwords, tokens, and test data are all visible in plain text to anyone on the path.
TLS (Transport Layer Security) solves two problems:
Problem 1: Encryption — nobody between browser and server can read the data.
Problem 2: Authentication — you're certain you're talking to attemptiq.com and not an impostor.
Most engineers know TLS adds encryption. The authentication part is equally important and less understood.
Certificates — The Authentication Mechanism
When your server sends its TLS certificate to the browser, the certificate contains:
- The domain name it's for (
attemptiq.com) - Your server's public key
- A digital signature from a Certificate Authority (CA)
A Certificate Authority is a company (Let's Encrypt, DigiCert, etc.) that browsers inherently trust. When your browser sees a certificate signed by Let's Encrypt, it knows: "Let's Encrypt verified this server really is attemptiq.com and issued this certificate." It trusts the certificate because it trusts Let's Encrypt.
The chain of trust:
Root CA (pre-installed in your OS/browser — you trust this unconditionally)
└── Intermediate CA (signed by Root CA)
└── Your certificate for attemptiq.com (signed by Intermediate CA)
Browsers verify this entire chain before proceeding. If any link is broken — certificate expired, wrong domain, self-signed without a trusted CA — you see the "Your connection is not private" warning.
The TLS Handshake (TLS 1.3 — What You're Actually Using)
After the TCP handshake, the TLS handshake happens. In TLS 1.3 (the modern version), it takes exactly one round trip:
How the encryption key is agreed on without sending it:
This is the magic part. Browser and server need to agree on a secret key to encrypt data. But they can't just send the key — an attacker could intercept it.
They use ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) key exchange. The math is complex but the intuition is: both sides exchange public information, and from that public information, both independently compute the same secret key. An attacker who sees all the exchanged public information cannot compute the secret. This is mathematically proven hard.
The resulting session key is a symmetric key (one key encrypts and decrypts). All actual data is encrypted with this using AES-256 — one of the fastest encryption algorithms known. Hardware acceleration exists for it on modern CPUs. This is why HTTPS doesn't noticeably slow things down.
Why symmetric for data, asymmetric for key exchange?
Asymmetric crypto (RSA, ECDHE) is mathematically expensive. Symmetric crypto (AES) is extremely fast — CPUs have dedicated hardware for it. You use asymmetric crypto once to securely agree on a symmetric key, then use that symmetric key for all actual data. Best of both worlds.
The vocabulary:
TLS= Transport Layer Security. Replaced SSL (which is dead).Certificate= public key + domain + CA signature.CA= Certificate Authority. A trusted third party that vouches for certificates.Chain of trust= root CA → intermediate CA → your certificate.ECDHE= key exchange algorithm. Lets two parties agree on a secret without sending it.AES-256= symmetric encryption algorithm. What actually encrypts your data.Session key= the symmetric key derived per-connection. Thrown away when connection closes.mTLS= Mutual TLS. Both sides present certificates. Used between microservices.
The Total Handshake Cost So Far
For a new HTTPS connection from scratch:
DNS resolution: ~30ms (if not cached)
TCP handshake: 1 RTT (~150ms India → Germany)
TLS handshake: 1 RTT (~150ms)
HTTP request: 1 RTT (~150ms)
──────────────
Total: ~480ms before your Node.js code runs
This is why connection reuse matters enormously. HTTP/1.1 keep-alive, HTTP/2 multiplexing, and connection pooling exist to pay this cost once and amortize it across many requests. Every keep-alive connection that handles 100 requests saved 99 × 300ms = 30 seconds of handshake overhead per user session.
Part 4: HTTP — What the Bytes Actually Look Like
TCP and TLS give you a reliable encrypted pipe. HTTP is the language spoken through that pipe.
HTTP is, at its core, a text protocol. Before HTTP/2, you could literally read it if you could intercept it. Let's look at what your browser actually sends.
The HTTP Request — Byte for Byte
GET /dashboard HTTP/1.1\r\n
Host: attemptiq.com\r\n
User-Agent: Mozilla/5.0 (Macintosh...) Chrome/120\r\n
Accept: text/html,application/xhtml+xml\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: en-IN,en;q=0.9\r\n
Cookie: session=eyJhbGci...\r\n
Connection: keep-alive\r\n
\r\n
That's it. A bunch of text lines separated by \r\n (carriage return + newline), then a blank line (\r\n\r\n) to signal end of headers, then optionally a body.
Every part has a purpose:
GET /dashboard HTTP/1.1 — the request line. Method, path, version.
Host: attemptiq.com — mandatory in HTTP/1.1. Because one IP can serve many domains. Your Nginx reads this to know which app to route to.
Cookie: session=... — your JWT or session token. This is how your server knows who's logged in. It's just a header. Nothing magic.
Connection: keep-alive — "don't close the TCP connection after this request, I have more."
Accept-Encoding: gzip, br — "I can accept compressed responses." Your server should compress responses. Dramatically reduces payload size.
The HTTP Response — Byte for Byte
HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
Content-Length: 348\r\n
Cache-Control: no-cache\r\n
ETag: "d41d8cd98f00b204e9800998ecf8427e"\r\n
Set-Cookie: session=eyJhbGci...; HttpOnly; Secure; SameSite=Strict\r\n
\r\n
{"user":{"id":"abc","name":"Trilok"},"tests":[...]}
Status codes are a state machine, not a lookup table. Know them physically:
200 OK— request succeeded, here's the response body201 Created— POST succeeded, a new resource was created204 No Content— request succeeded but there's nothing to return (DELETE success)301 Moved Permanently— this URL is gone forever, update your bookmarks302 Found— temporary redirect, same URL next time304 Not Modified— "the ETag matched, your cached copy is still fresh, no body sent"400 Bad Request— your request syntax is wrong401 Unauthorized— you need to authenticate (no token or expired token)403 Forbidden— you're authenticated but not authorized for this resource404 Not Found— resource doesn't exist429 Too Many Requests— rate limited500 Internal Server Error— your code crashed502 Bad Gateway— your Nginx couldn't reach your Node.js app (app is down)503 Service Unavailable— server overloaded or down for maintenance504 Gateway Timeout— your Nginx reached Node.js but Node.js took too long to respond
The 401 vs 403 distinction matters in interviews:
- 401: "I don't know who you are." Send credentials.
- 403: "I know who you are. You're just not allowed."
HTTP/2 — What Changed and Why
HTTP/1.1 has a critical flaw: you can only have one request in-flight at a time per TCP connection. If you want to load 30 resources (HTML, CSS, JS, images), you either:
- Open 30 TCP connections (browsers cap at ~6 per domain), or
- Wait for each response before sending the next request
HTTP/2 (2015) fixed this with multiplexing. Multiple requests and responses can be interleaved on a single TCP connection. Each request/response is called a stream. Streams have IDs. The receiver reassembles them.
HTTP/2 also introduced:
- Header compression (HPACK) — headers are often redundant across requests. HPACK compresses them.
- Server push — server can proactively send resources it knows the client will need. (In practice, rarely used effectively.)
- Binary framing — HTTP/2 is no longer plain text. It's a binary protocol.
HTTP/2 is what your Nginx and modern browsers use by default.
HTTP/3 — The Future (Already Present)
HTTP/3 replaces the TCP layer entirely with QUIC — a protocol built on UDP.
The reason: TCP's head-of-line blocking (one lost packet blocks everything) was still a problem in HTTP/2, now affecting all multiplexed streams at once. QUIC implements its own reliability per-stream in UDP, so a lost packet in stream 1 doesn't block stream 2.
Cloudflare, Google, and most CDNs already serve HTTP/3. When a user hits your Cloudflare-fronted site, the Cloudflare → browser connection is likely HTTP/3. The Cloudflare → your Hetzner server connection is still HTTP/2 or HTTP/1.1.
Part 5: UDP — Why It Exists
We've been talking about TCP's reliability mechanisms. All that reliability has a cost: latency. Every lost packet must be retransmitted. Every connection needs a handshake.
UDP (User Datagram Protocol) strips all of that out.
UDP is connectionless. No handshake. No acknowledgments. No ordering. No retransmission. You send a packet, it may or may not arrive, you'll never know.
Why would you ever want this?
Because for some applications, late data is worse than no data.
In a video call, if a packet carrying 20ms of audio is lost, retransmitting it would arrive too late to be useful — the conversation has moved on. Better to skip that 20ms and keep going than to freeze while waiting for the retransmit. UDP lets the application handle this itself, or simply drop it.
Where UDP is used:
- DNS queries — tiny, fast, retry is fine, no need for connection overhead
- Video calls (WebRTC) — latency matters more than perfect quality
- Online games — your character's position must arrive now or be thrown away
- Video streaming (QUIC/HTTP/3) — built on UDP with selective reliability
- NTP (time synchronization) — simple request/response, retrying is fine
The vocabulary:
UDP= User Datagram Protocol. Connectionless, unreliable, fast.Datagram= a UDP packet. Self-contained, may be lost.QUIC= Google's protocol. UDP underneath, with selective reliability, 0-RTT reconnection.WebRTC= browser-to-browser communication (video calls). Uses UDP via QUIC/SRTP.
TCP vs UDP — The Real Decision
| TCP | UDP | |
|---|---|---|
| Connection | Handshake required | No handshake |
| Reliability | Guaranteed delivery | Best effort |
| Ordering | Maintained | Not guaranteed |
| Speed | Slower (overhead) | Faster |
| Use when | You must not lose data | Latency matters more than loss |
| Examples | HTTP, DB queries, email | DNS, video calls, games |
Part 6: The Complete Picture
Now you can narrate the full journey from zero:
Notice: Nginx terminates TLS. Your Node.js code never sees encrypted traffic. Nginx decrypts everything and forwards plain HTTP on localhost. This is why you can write req.headers in Node without doing any crypto — Nginx already handled it.
This also means traffic between Nginx and Node.js is unencrypted (just localhost, so that's fine). But if your Node.js is on a different machine from Nginx, you'd need to think about that.
Part 7: The Vocabulary Map
| Word | What it actually is |
|---|---|
DNS | System that maps domain names to IP addresses. Hierarchical. Cached. |
Recursive Resolver | Does the DNS walking for you. Your ISP or 8.8.8.8. |
Authoritative NS | The nameserver that actually holds your DNS records. Cloudflare in your case. |
TTL | How long resolvers cache a DNS answer. Lower = faster propagation. Higher = less load. |
A Record | DNS record mapping domain to IPv4 address. |
IP | Internet Protocol. Routes packets. Unreliable by design. |
TCP | Transport layer. Adds reliability, ordering, flow control on top of IP. |
RTT | Round Trip Time. Packet out + ACK back. Base unit of network latency. |
Three-way handshake | SYN → SYN-ACK → ACK. 1 RTT cost to establish TCP. |
Sequence number | Per-byte counter. Foundation of TCP reliability. |
Retransmission | Resending lost packets. Makes TCP reliable at a latency cost. |
Head-of-line blocking | Later packets stuck behind a lost earlier one. TCP's core flaw. |
Connection pooling | Keeping TCP connections alive and reusing them. Avoids handshake per-query. |
TLS | Encryption + authentication layer on top of TCP. |
Certificate | Public key + domain + CA signature. Proves server identity. |
ECDHE | Key exchange algorithm. Derives shared secret without sending it. |
AES-256 | Symmetric encryption. What actually encrypts your data. Very fast. |
HTTP | Application-layer protocol. Text-based request/response over TCP+TLS. |
Status codes | Numeric response codes. Not arbitrary — each has a precise meaning. |
Keep-alive | Reuse TCP connection across multiple HTTP requests. |
HTTP/2 | Multiplexed HTTP over one TCP connection. Binary framing. Header compression. |
HTTP/3 / QUIC | HTTP over UDP. Solves head-of-line blocking. Already in use at CDNs. |
UDP | Connectionless, unreliable, fast. For latency-sensitive applications. |
mTLS | Both sides present certificates. Used between internal services. |
Part 8: Practice Problems
Problem 1 (System Design): You've deployed AttemptIQ. A user in Chennai reports the first page load takes 900ms but subsequent loads are fast. Explain every reason why the first load is slow and subsequent ones are fast, at the network layer. Be specific — which caches, which connections are warmed, which handshakes are skipped.
Problem 2 (Architecture): You want to add a real-time feature to AttemptIQ: live quiz mode where 500 students see the same question simultaneously and their answers appear on the teacher's screen in real time.
You're choosing between:
- Option A: Each student polls
GET /live-quiz/answersevery 1 second - Option B: WebSockets (long-lived TCP connection per student)
- Option C: Server-Sent Events (one-way stream from server to student)
For each option: what does the TCP connection pattern look like? What's the server load at 500 students? What would you choose and why?
Problem 3 (Reasoning): You lower your DNS TTL from 3600 to 60 before a server migration. A colleague asks: "Won't that cause 60× more DNS traffic to your authoritative nameserver?"
Are they right? Think through what "60× more traffic" means physically — which servers in the DNS chain get more load, which don't, and why.
Problem 4 (Debug):
Your users report that attemptiq.com loads fine but API calls to api.attemptiq.com intermittently return 502 Bad Gateway. This only happens under load (50+ concurrent users).
Walk through every layer we've covered — DNS, TCP, TLS, HTTP, Nginx, Node.js — and for each one, describe what could cause a 502 specifically at that layer. Which layer is most likely?
Problem 5 (Go Deeper): You've learned that TLS 1.3 takes 1 RTT. There's also a feature called 0-RTT (Zero Round Trip Time resumption).
Based on what you know about how TLS works, reason about how 0-RTT might be possible. What would you need to have saved from a previous connection? What security risk might this introduce?
(Don't look this up. Try to reason it from first principles first.)
What's Coming in Chapter 3
You now understand how a request travels from a browser to your server. You understand the physical cost of every layer.
Chapter 3 is where we go inside the database — the place where most production bugs actually live.
We'll cover:
- How Postgres stores your data physically on disk (pages, heap files, TOAST)
- How indexes work from the ground up — B-trees, not as vocabulary but as a data structure you can draw
- What
VACUUMis and why it exists - Transactions and ACID — what actually happens when you
BEGIN - How the query planner decides what to do with your SQL
- Connection pooling revisited — why
pgBouncerexists and whatpg's pool doesn't do
After Chapter 3, you will be able to diagnose any slow Postgres query, explain any locking issue, and design schemas that don't collapse at scale.
End of Phase 1, Chapter 2. Estimated reading time: 60-75 minutes. Practice problems: 45-60 minutes. Do not proceed to Chapter 3 until you can answer Problem 4 — the 502 debug — in full, layer by layer.