Basic Components6 min · 6 of 64

Clients and the constraints they impose

Size a client retry storm, decide what work belongs on the device, and keep an API compatible with app versions you cannot force off a user's phone.

A client is any entity that initiates a request to a server: the requester in a client-server architecture. It is also the one part of the system you do not operate — it runs on hardware you did not pick, over a network you cannot measure, in a version you cannot force off a user's phone. Almost everything hard about client design follows from that asymmetry.

What counts as a client

Web browsers are the most common, requesting pages, images and API responses over HTTP/HTTPS. They are the easy case: ship a new bundle and the fix is live within a cache TTL.

Mobile apps talk to servers through APIs rather than fetching rendered pages. A released version can stay installed for years, so the API contract it depends on effectively becomes permanent.

Desktop applications — mail clients, games, editors — connect for sync and updates. They share the mobile upgrade problem across a wider spread of OS, proxy and firewall behaviour.

Other services. In a microservices architecture, one service calling another is a client — the most common kind in a distributed system, and the most dangerous: a human gives up and closes the tab, while a service client retries in a tight loop at machine speed.

IoT devices — thermostats, meters, sensors — are thin clients on flaky, battery-powered links that batch and sleep rather than hold a connection open, so the server sees bursts.

When you type "www.google.com" into your browser and press Enter, the browser is the client: it resolves the name through DNS, opens a connection, sends one request, and renders the response. Every box after it is a server reacting to a decision the client made.

The request path, and where it multiplies

Every client type funnels through one entry point. The mobile app timing out and retrying three times with no backoff turns one slow request into four — which is how clients take servers down.
Three kinds of client behind one gateway, and a retry storm from the mobile appretry ×3,no backoffmisstimeout at 2 sBrowserMobile appAnotherserviceAPI gatewayApp serverCachePostgres

Scroll to zoom · drag to pan · 0 fits · Esc closes

The solid edges are what most candidates draw. The dotted loop takes the system down: the client, not the server, decides how many requests exist.

Work the numbers: one million daily active users at 20 requests a day.

1,000,000 DAU x 20 req/day = 20,000,000 req/day
20,000,000 / 86,400 s      ~= 230 QPS average
230 QPS x 4 (peak factor)  ~= 1,000 QPS peak

At 1,000 QPS peak there is 5x headroom: one commodity Postgres box handles roughly 5,000 simple QPS. Now a dependency slows and p99 crosses the client's 2-second timeout, so the client SDK retries three times, immediately, with no backoff:

1,000 QPS x 4 attempts = 4,000 QPS offered

Headroom is gone, from a slowdown that never changed user behaviour. Little's Law shows what breaks first: concurrency = arrival rate x latency, so 4,000 QPS x 2 s = 8,000 requests in flight against a connection pool sized for a couple of hundred. The pool exhausts, queueing pushes otherwise-healthy requests past the timeout, and those retry too. The slowdown becomes an outage that outlives its cause.

Three corrections, in order of value. Exponential backoff with jitter spreads the second attempt instead of synchronising every client onto the same instant. A retry budget caps retries at roughly 10% of live traffic, so total failure produces 1.1x load rather than 4x. A circuit breaker stops sending to a dependency that is already failing. And since you cannot patch an installed client, server-side rate limiting is the backstop for when the client-side policy is wrong.

What belongs on the client

Clients carry real logic — JavaScript in a browser, native code in an app — for rendering, input validation, optimistic updates and local caching. The argument for pushing work outward is latency, and it is arithmetic. A phone in India calling a server in US East pays about 200 ms per round trip, so a screen that makes six sequential dependent calls spends 6 x 200 ms = 1.2 s on the network before the server does any work.

Two ways out, and it matters which you pick. If the calls are independent, issue them in parallel and pay one round trip plus the slowest response. If each depends on the previous one, parallelism buys nothing — aggregate them behind the API gateway so the hops happen inside the datacentre at 0.5 ms each instead of across an ocean at 200 ms. Leaving the fan-out on the device, the rejected alternative, is simpler to build and puts your latency budget at the mercy of the carrier.

State is the other placement decision. Give the client a token it sends on every request and app servers hold nothing per user, which is what makes them stateless and horizontally scalable. Session data in server memory, the rejected alternative, forces sticky sessions and loses every session on an instance when it restarts.

What never moves to the client is trust. Client-side validation is user feedback: it runs on a device an attacker controls and is bypassed with a hand-written request. Authorisation, quota enforcement and validation are server-side checks, repeated even when the client already ran them.

Versions you cannot recall

Assume a mobile release reaches roughly 90% of users in a month, with a tail that runs for a year or more. That makes the API contract additive by default: add fields, never remove or repurpose them, and version the endpoint when the shape must change. See REST API design.

The failure mode is specific. Delete a field an old client dereferences without a null check and that version crashes on launch. Users relaunch, sending the request again, so the crash itself generates a retry storm — and the fix waits on an app store review.

In an interview

The interviewer is testing whether you treat the client as an active participant with its own failure behaviour, or as a box on the left labelled "user". Most candidates skip it, so it is cheap depth.

Say three things while scoping. The client mix and its cost: "mobile-first, users in India, servers in US East, so 200 ms per round trip and I am designing for few of them." The retry policy, unprompted: "clients retry with exponential backoff and jitter under a 10% retry budget, and the gateway rate-limits as the backstop." And where state lives: "the client holds the token, app servers stay stateless."

Two mistakes lose points reliably. Designing an API you assume you can change at will — "we will just update the clients" says you have never shipped a mobile release. And specifying retries with no backoff and no cap, then not noticing that your own retry policy turned a slow dependency into a 4,000 QPS stampede.

Check yourself

1. Your mobile client retries twice on any 5xx with no backoff. Peak is 1,000 QPS, the client timeout is 2 s, and a dependency degrades for 60 seconds. What is the offered load, and what breaks before the database?

Three attempts per request: 1,000 x 3 = 3,000 QPS offered. The connection pool goes first. By Little's Law, 3,000 QPS x 2 s = 6,000 requests in flight against a pool of a few hundred. Requests queue, queueing pushes them past the 2-second timeout, and those retry too — so the system stays saturated after the dependency recovers.

2. A profile screen needs five API calls. Users are in India, servers in US East, and the screen's p99 budget is 500 ms. Do you ship it as designed?

No. Five sequential round trips at ~200 ms is 1 s of network, double the budget before any server work. If the calls are independent, run them in parallel: one round trip plus the slowest handler, roughly 250 ms, which fits. If they are dependent, aggregate them into one endpoint so the chaining happens in the datacentre at ~0.5 ms per hop.

3. You need to remove a response field. Telemetry says 12% of your 1 million daily active users run a version that reads it without a null check. What do you do?

Do not remove it. 12% of 1M DAU is 120,000 users in a crash-and-relaunch loop, and the fix ships only after an app store review. Keep serving the field, add the new shape alongside it, deprecate the old one, and delete it when that version's share drops under your error budget.