Client-Server & Networking Recap
By the end of this lesson you will be able to trace what happens when a phone app talks to a server, name the protocols involved, and count the network round trips that make a request slow or fast. Every later design in this course builds on this picture.
Clients and servers
A client is the program that asks for something: a browser, a mobile app, or even another server. A server is the program that answers. The pattern is called client-server.
Think of a dhaba. You (the client) place an order. The kitchen (the server) prepares it. The waiter carries messages both ways. You never walk into the kitchen yourself. In software, the "waiter" is the network, and the messages follow agreed rules called protocols.
Here is the path of one request, drawn as boxes:
Phone app Data centre
+---------+ 1. "Where is api.shop.in?" +-------------+
| Client | -----------------------------> | DNS resolver|
| | <----------------------------- | |
| | 2. "It is 203.0.113.10" +-------------+
| |
| | 3. TCP + TLS handshake +-------------+
| | <============================> | Web server |
| | 4. HTTP request / response | (API) |
+---------+ +------+------+
|
5. SQL query
v
+-------------+
| Database |
+-------------+Each arrow costs time. The rest of this lesson explains each one.
DNS: the phone book of the internet
Computers find each other by IP address, a number such as 203.0.113.10. People remember names such as api.shop.in. DNS (Domain Name System) turns names into addresses.
Your phone asks a resolver, usually run by your internet provider or a public DNS service. The resolver asks the DNS servers responsible for the name and returns the answer. Each answer carries a TTL (time to live): the number of seconds it may be cached. Caching is why the second visit to a site is faster.
Step 1: run the model below. It counts how often a resolver must do the slow full lookup.
class Resolver:
def __init__(self, records):
self.records = records # name -> (ip, ttl_seconds)
self.cache = {} # name -> (ip, expires_at)
self.slow_lookups = 0
def resolve(self, name, now):
if name in self.cache and self.cache[name][1] > now:
return self.cache[name][0], "cache"
ip, ttl = self.records[name]
self.slow_lookups += 1 # walk the DNS hierarchy: tens of ms
self.cache[name] = (ip, now + ttl)
return ip, "full lookup"
r = Resolver({"api.shop.in": ("203.0.113.10", 300), "img.shop.in": ("198.51.100.7", 60)})
for t, name in [(0, "api.shop.in"), (5, "api.shop.in"), (30, "img.shop.in"),
(95, "img.shop.in"), (299, "api.shop.in"), (301, "api.shop.in")]:
ip, how = r.resolve(name, t)
print(f"t={t:>3}s {name:12} -> {ip:13} ({how})")
print("slow lookups:", r.slow_lookups)t= 0s api.shop.in -> 203.0.113.10 (full lookup) t= 5s api.shop.in -> 203.0.113.10 (cache) t= 30s img.shop.in -> 198.51.100.7 (full lookup) t= 95s img.shop.in -> 198.51.100.7 (full lookup) t=299s api.shop.in -> 203.0.113.10 (cache) t=301s api.shop.in -> 203.0.113.10 (full lookup) slow lookups: 4
Step 2: notice the trade-off. A long TTL means fewer slow lookups. A short TTL means that when you move a server to a new address, users find the new address sooner. Teams often lower the TTL a day before a planned migration for exactly this reason. DNS is also used for simple load spreading, which you will see in Load Balancing. The DNS lesson in the networks course goes deeper.
TCP and UDP
Once the client knows the address, it needs a way to send bytes.
- TCP gives a reliable, ordered stream. Lost packets are resent. Before any data moves, the two sides do a handshake that costs one round trip. Most web, database and API traffic uses TCP.
- UDP just sends packets. No handshake, no resending, no ordering. It suits live video calls, games and DNS queries, where a late packet is useless anyway.
A round trip time (RTT) is the time for a message to go to the server and a reply to come back. From Pune to a server in Mumbai it might be a few milliseconds. From Pune to a server in the USA it is often more than 200 ms, because light in fibre is fast but not instant. Distance is a design input.
HTTP and HTTPS
HTTP is the language of web requests. A request has a method, a path, headers and an optional body. A response has a status code, headers and a body.
GET /api/orders/8812 HTTP/1.1
Host: api.shop.in
Accept: application/json
Authorization: Bearer <token>
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, max-age=0
{"id": 8812, "status": "OUT_FOR_DELIVERY", "eta_minutes": 12}HTTPS is HTTP inside TLS, the protocol that encrypts traffic and proves the server's identity with a certificate. TLS 1.3, the current version, needs one round trip for a fresh handshake. Every public API today should use HTTPS.
HTTP has three main versions in use:
| Version | Runs over | Key idea |
|---|---|---|
| HTTP/1.1 | TCP | One request at a time per connection; browsers open several connections |
| HTTP/2 | TCP | Many requests share one connection at once (multiplexing) |
| HTTP/3 | QUIC over UDP | Like HTTP/2, but a lost packet does not stall the other requests; transport and TLS handshakes are combined |
Counting round trips
Round trips add up. Let us count them for a fresh HTTPS request and for a request on a connection that is already open.
def time_to_first_byte(rtt_ms, setup_rtts, server_ms=20):
return setup_rtts * rtt_ms + rtt_ms + server_ms # setup + request/response + work
cases = [
("New TCP + TLS 1.3 connection", 2), # 1 RTT TCP, 1 RTT TLS
("New TCP + TLS 1.2 connection", 3), # TLS 1.2 needs 2 RTTs
("New QUIC (HTTP/3) connection", 1), # transport + TLS together
("Reused keep-alive connection", 0),
]
for rtt in [10, 80, 220]:
print(f"RTT {rtt} ms")
for label, setup in cases:
print(f" {label:30} {time_to_first_byte(rtt, setup):4} ms")RTT 10 ms New TCP + TLS 1.3 connection 50 ms New TCP + TLS 1.2 connection 60 ms New QUIC (HTTP/3) connection 40 ms Reused keep-alive connection 30 ms RTT 80 ms New TCP + TLS 1.3 connection 260 ms New TCP + TLS 1.2 connection 340 ms New QUIC (HTTP/3) connection 180 ms Reused keep-alive connection 100 ms RTT 220 ms New TCP + TLS 1.3 connection 680 ms New TCP + TLS 1.2 connection 900 ms New QUIC (HTTP/3) connection 460 ms Reused keep-alive connection 240 ms
This is a simplified model, but the lesson is real. Two design rules follow:
- Reuse connections. Keep-alive connections and connection pools to databases avoid repeated handshakes.
- Move servers closer to users. When RTT is large, every round trip hurts. This is why CDNs place servers in many cities, covered in CDNs & Distributed Caches.
Real-time updates: polling and WebSockets
Normal HTTP is request-response: the server cannot speak first. For live data, such as a delivery rider's location, there are four common choices:
- Short polling: the client asks every few seconds. Simple, but wasteful when nothing changed.
- Long polling: the client asks, and the server holds the request open until there is news or a timeout.
- WebSockets: one long-lived connection that both sides can send on at any time. Chat apps use this; see Design a Chat App.
- Server-Sent Events (SSE): a one-way stream from server to browser over plain HTTP, good for live scores and notifications.
Common mistakes
- Ignoring distance. A design that makes five sequential calls to a far-away region will be slow, however fast each server is.
- Opening a new connection for every database query. Use a connection pool.
- Setting very long DNS TTLs and then needing to move servers quickly.
- Polling every second from millions of phones when a push channel would do.
Key points
- Clients ask, servers answer, and protocols are the agreed rules in between.
- DNS turns names into IP addresses, and TTLs control how long answers are cached.
- TCP is reliable and ordered; UDP is fast and simple; QUIC builds a reliable transport on UDP for HTTP/3.
- HTTPS is HTTP over TLS; TLS 1.3 needs one round trip for a new handshake.
- Round trips dominate latency over long distances, so reuse connections and serve users from nearby.
- WebSockets and SSE let servers push updates without polling.
Exercise
A Lucknow user calls an API in a far region with an RTT of 150 ms. The page needs 4 API calls, made one after another, and each call does 30 ms of server work. Using the same model as the lesson, compute the total time if every call opens a new TCP + TLS 1.3 connection, and if all calls share one reused connection (only the first call pays the setup).
Show solution
rtt, work, calls = 150, 30, 4
new_each_time = calls * (2 * rtt + rtt + work)
reused = (2 * rtt) + calls * (rtt + work)
print("new connection per call:", new_each_time, "ms")
print("one reused connection :", reused, "ms")new connection per call: 1920 ms one reused connection : 1020 ms
Reusing the connection saves almost a second. Making the four calls in parallel, or combining them into one API call, would save even more.
Quick Quiz
1. What does DNS do?
DNS is the internet's phone book: names in, addresses out.
2. Why can a reused connection be faster than a new one?
Each handshake costs at least one round trip, which a reused connection avoids.
3. Which choice suits a chat app where both sides send messages at any time?
A WebSocket is one long-lived, two-way connection.
4. What runs underneath HTTP/3?
HTTP/3 uses QUIC, which runs over UDP and combines the transport and TLS handshakes.
Learn System Design with a mentor
Free lessons get you started. Our Computer Science Fundamentals training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
