Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
Lesson 5 · APIs and Communication
Synchronous vs Asynchronous Communication: Queues, Polling, WebSockets and Server-Sent Events
When should a caller wait for an answer, and when should work happen later? Learn the costs and benefits of synchronous calls and asynchronous processing, the patterns for returning results later (202 and status polling, webhooks, push), and how to choose between short polling, long polling, Server-Sent Events, WebSockets and mobile push.
For
Beginners and interview candidates deciding how components should communicate, and developers who need to choose a real-time update mechanism.
Level
Before you start
Read How a Web Request Travels (article 03) and API Design (article 04), especially 202 Accepted and idempotency. Article 02's section on availability in series is useful background.
On this page
Key takeaways
- Make a call synchronous only when the caller needs the result to continue; move slow, unreliable or non-essential work to the background.
- Asynchronous processing gives fast responses, absorbs spikes and isolates failures, but adds delay, duplicates and more moving parts to monitor.
- Return results of background work with 202 plus a status resource, a webhook, or a push to the client.
- Short polling is simple and cache-friendly, long polling and Server-Sent Events push with less waste, and WebSockets give two-way real-time at the cost of stateful connections.
- Every real-time channel must handle disconnects: clients reconnect with backoff and resume from the last event they saw.
A user uploads a photo to Pixly. Behind that one tap, the system must store the file, save its metadata, create three thumbnail sizes, scan the image for policy violations, and tell the uploader's followers. If the upload request does all of that before replying, the user stares at a spinner for several seconds, and if the follower-notification step fails, the whole upload fails with it, even though the photo itself was saved perfectly well.
The fix is to ask a simple question about every piece of work: does the caller need the result right now? If yes, do it while they wait. If no, accept the request, reply quickly, and do the rest in the background. This article is about making that decision well, and about the ways to deliver results and live updates back to users.
What you will learn
#- What synchronous and asynchronous communication mean, and what each costs.
- A decision guide for when to wait and when to defer work.
- Patterns for returning the result of background work: status polling, webhooks and push.
- Short polling, long polling, Server-Sent Events, WebSockets and mobile push notifications, compared.
- The failure modes of each approach and how to handle them.
- Two worked examples: Pixly's upload flow and a live-score page with millions of viewers.
Synchronous communication
#In synchronous communication the caller sends a request and waits for the response before continuing. An ordinary HTTP API call is synchronous from the caller's point of view.
Client ----request----> Service A ----request----> Service B <---response---- <---response---- (client waits the whole time)Why it is attractive:
- Simple to write and reason about: call, get answer, continue.
- Errors come back immediately, to the code that can handle them.
- The caller knows the outcome when the call returns.
What it costs:
- Latency adds up. If A calls B, which calls C, the user waits for all three in sequence.
- Availability multiplies. As article 02 showed, every synchronous dependency in the path lowers the availability of the whole chain.
- Coupling in time. Both sides must be up and fast at the same moment. A slow B makes A slow; a down B makes A fail.
- Load passes straight through. A traffic spike at the front hits every service in the chain at the same instant.
Asynchronous communication
#In asynchronous communication the caller hands off work and does not wait for it to finish. Usually the work is written to a message queue (article 11), and the caller gets a quick acknowledgement that the work was accepted. Workers pick up messages and do the work later, often within milliseconds or seconds.
Client --request--> API server --put message--> Queue --> Worker (does the slow work) <--202 / 201-- (replies at once)Why it is attractive:
- Fast responses. The user gets an answer as soon as the essential part is done.
- Spikes are absorbed. The queue acts as a buffer: if 10,000 uploads arrive in a minute, workers process them at their own steady pace instead of the system collapsing. Microsoft describes this as the queue-based load leveling pattern (Queue-Based Load Leveling).
- Failures are isolated. If the notification provider is down, uploads still succeed; notification messages wait in the queue and are retried.
- Independent scaling. You can add workers without touching the API servers.
What it costs:
- Delay. The work is done "soon", not "now". Users may see "processing" states.
- The result must be delivered somehow (the next section).
- Duplicates and ordering. Most queues deliver a message at least once, so a worker may see it twice; messages may not arrive in order (article 20).
- More moving parts. A queue to run and monitor, workers to deploy, a backlog that can grow silently.
- Harder debugging. One user action now spans several processes and times; you need tracing (article 25).
When to wait and when to defer
#Ask these questions about each step of a request:
- Does the caller need the result to continue? Login must tell you whether you are logged in. Sending a welcome email does not need to finish before you see your dashboard.
- Is the step slow? Anything that can take more than a few hundred milliseconds (image or video processing, report generation, calls to slow third parties) is a candidate for the background.
- Is the step unreliable? Calls to external providers (email, SMS, payment confirmations) fail and need retries; retrying inside a user's request makes them wait and multiplies load.
- Can it be retried safely later? Background work must be idempotent or deduplicated, because it will sometimes run twice.
- Is the load spiky? Bursty work benefits from a buffer.
Operation | Sync or async | Reasoning |
|---|---|---|
Log in | Sync | The user cannot continue without the answer |
Check stock and reserve it at checkout | Sync | The answer decides what the user sees next |
Save a photo's metadata | Sync | The uploader must get an id and see the photo |
Create thumbnails | Async | Slow; the original can be shown meanwhile |
Send an order confirmation email | Async | Not needed to continue; the provider can fail |
Update a search index after an edit | Async | A few seconds of delay is acceptable |
Charge a card | Usually sync for authorisation, async for final confirmation | The user needs to know whether payment was accepted, but providers often confirm later via webhooks (article 39) |
Generate a yearly tax report | Async | Can take minutes |
Most real requests are a mix: a synchronous core that does the minimum the user needs, and asynchronous follow-up work. The art is keeping the synchronous core small.
Getting the result of background work back
#If work happens later, how does anyone learn the outcome? Three common patterns.
Pattern 1: 202 Accepted and a status resource
#The API accepts the job, returns 202 Accepted and a URL to check progress (article 04 introduced this):
POST /v1/exports -> 202 Accepted, Location: /v1/exports/exp_31GET /v1/exports/exp_31 -> {"status": "running"}GET /v1/exports/exp_31 -> {"status": "done", "download_url": "..."}Simple, works with any client, and the status resource is also useful for support staff. The client must poll, which is the subject of the next section.
Pattern 2: Webhooks (server-to-server callbacks)
#When the caller is another system, your service can call its URL when the work completes. Payment providers use this heavily: the provider sends an HTTP request to the merchant's server when a payment succeeds or fails. Stripe's documentation, for example, describes webhooks as the way to receive events from its platform and asks receivers to verify signatures and handle duplicate events (Webhooks).
If you send webhooks, plan for receivers being down: retry with backoff, sign each payload so receivers can verify it came from you, and include an event id. If you receive webhooks, verify the signature, respond quickly (do the processing asynchronously), and make the handler idempotent, because the same event may arrive more than once.
Pattern 3: Push to the client
#For people using an app, the most pleasant option is to push the result: a notification, or a live update on screen. The next section covers how.
Delivering updates to clients: five mechanisms
#HTTP was designed for the client to ask and the server to answer. Real-time features need the server to tell the client something it did not ask for at that moment. There are five common ways.
1. Short polling
#The client asks every few seconds: "anything new?"
Client: GET /notifications?since=... (every 5 s)Server: 200 [] ... 200 [] ... 200 [one item] ...- Pros: trivial to build; works everywhere; each request is an ordinary stateless HTTP call, so it scales with normal load balancers and can be cached by a CDN when every client asks the same question.
- Cons: wasteful when updates are rare (most responses are empty); updates arrive late by up to one polling interval.
The waste is easy to estimate. One million connected users polling every 5 seconds generate 1,000,000 / 5 = 200,000 requests per second, almost all returning nothing.
2. Long polling
#The client asks, and the server holds the request open until there is something to send or a timeout (say 30 seconds) passes. Then the client immediately asks again.
- Pros: near-real-time delivery without new protocols; far fewer empty responses than short polling.
- Cons: the server holds many open requests, which needs an efficient, non-blocking server; timeouts in proxies and load balancers must be configured; each message still costs a new request.
3. Server-Sent Events (SSE)
#The client opens one HTTP connection, and the server keeps it open and streams events down it as text. The browser's EventSource API handles parsing and automatically reconnects, sending the id of the last event it received so the server can resume. SSE is specified in the HTML standard (Server-sent events).
- Pros: simple, one-way server push over plain HTTP; built-in reconnection and resume; works with HTTP/2.
- Cons: one direction only (client-to-server messages use normal requests); text-based; each client holds an open connection.
4. WebSockets
#A WebSocket starts as an HTTP request and then upgrades to a persistent, full-duplex connection: both sides can send messages at any time with very little overhead per message (RFC 6455, The WebSocket Protocol).
- Pros: true two-way, low-latency messaging; ideal for chat, collaborative editing, multiplayer games, live trading screens.
- Cons: stateful connections. Each connection lives on one specific server, which changes how you scale and deploy (below). You also build your own message format, acknowledgements and reconnection logic.
5. Mobile push notifications
#When a phone app is in the background or closed, it has no open connection. Mobile platforms provide their own push services, such as Firebase Cloud Messaging for Android (it can also deliver to iOS through Apple's service) and the Apple Push Notification service for iOS (Firebase Cloud Messaging). Your backend sends a message to the platform service, which delivers it to the device.
- Pros: reaches users when the app is not running; battery-friendly because the operating system manages one shared connection.
- Cons: delivery is best-effort and not instant; payload size is limited; you depend on a third party; users can disable notifications. Treat push as a nudge ("you have a new message"), and fetch the real data when the app opens.
Comparison
#Mechanism | Direction | Connection | Latency of updates | Server cost | Best for |
|---|---|---|---|---|---|
Short polling | Client asks | New request each time | Up to the interval | High if many clients and few updates; low if cacheable | Infrequent updates, simple clients, data the CDN can cache |
Long polling | Client asks, server waits | Held request, repeated | Near real time | Many open requests | Fallback where streaming is not available |
SSE | Server to client | One long-lived HTTP connection | Real time | One open connection per client | Live feeds, scores, progress, notifications in the browser |
WebSockets | Both ways | One long-lived upgraded connection | Real time | One open connection per client, plus routing | Chat, collaboration, games |
Mobile push | Server to device | Platform-managed | Seconds, best-effort | Low for you | Waking users when the app is closed |
Scaling long-lived connections
#SSE and WebSockets change the scaling picture, because a connection is pinned to one server for its whole life.
- Capacity is measured in concurrent connections, not only requests per second. A server holding 50,000 idle connections uses memory for each, even if no messages flow.
- Messages must find the right server. If user A's connection is on server 3 and user B sends A a message through server 7, server 7 must route it to server 3. Common solutions are a connection registry (user id to server, kept in a fast shared store) plus internal messaging, or a pub/sub system that every connection server subscribes to. Article 34 designs this for chat.
- Load balancers must allow long connections: idle timeouts long enough, and protocol upgrade support for WebSockets.
- Deployments disconnect users. Restarting a server drops all its connections. Drain connections gradually and let clients reconnect.
- Reconnection storms. If a server with 100,000 connections crashes, 100,000 clients reconnect at once. Clients must reconnect with exponential backoff and random jitter (article 20) so they spread out.
- Missed messages. While disconnected, a client misses events. Give events increasing ids, and let clients resume from the last id they saw, or fetch what they missed through a normal API.
Failure modes, side by side
#Approach | What goes wrong | Mitigation |
|---|---|---|
Synchronous chain | A slow dependency holds threads in every caller; overload spreads upstream (cascading failure) | Timeouts on every call, circuit breakers, fewer synchronous hops (article 23) |
Async with a queue | Backlog grows unnoticed; a malformed message fails forever; work runs twice | Monitor queue age and depth; dead-letter queue after N attempts; idempotent workers (articles 11 and 20) |
Webhooks | Receiver is down; events arrive twice or out of order | Retries with backoff, signatures, event ids, idempotent receivers |
Polling | Polling floods the API at peak; stale data between polls | Cache the polled resource; adaptive intervals; jitter |
WebSockets and SSE | Server crash drops thousands of connections; reconnection storm; missed events | Backoff with jitter, resume from last event id, spread connections across many servers |
Mobile push | Notifications delayed or dropped | Treat push as a hint; the app syncs on open |
Worked example 1: Pixly's upload flow
#Let us apply the decision guide to the photo upload from the introduction.
Step | Needed before replying? | Decision |
|---|---|---|
Authenticate the user, validate size and type | Yes | Sync |
Store the original file | Yes (we must not acknowledge an upload we have not saved) | Sync, directly to object storage via an upload URL (article 10) |
Save metadata (owner, caption, time, status "processing") | Yes | Sync |
Create thumbnails | No | Async |
Scan the image for policy violations | No, but the photo should stay hidden from others until it passes | Async, with status gating visibility |
Add the photo to followers' feeds | No | Async |
Notify followers | No | Async |
REQUEST PATH (synchronous, user waits) App --> Load balancer --> API server --> Database (metadata, status = processing) | +--> Queue: "photo.uploaded" (photo id) App <-- 201 Created {photoId, status: "processing"}ASYNCHRONOUS PATH (user does not wait) Queue --> Thumbnail workers ---> Object storage (thumbnails) --> Moderation workers --> Database (status = visible or rejected) --> Feed workers --------> Followers' feeds --> Notification workers -> Push providerThe uploader sees their photo immediately (marked as processing), so the "read your own writes" requirement from article 02 is met. Followers see it seconds later, which the requirements allowed. If the push provider is down, notifications wait in the queue and the upload is unaffected.
The sharp edge: the API writes to the database and puts a message on the queue. If the server crashes between the two, the photo exists but no thumbnails are ever made. Article 21 solves this "dual write" problem with the transactional outbox pattern. For now, notice that splitting work into sync and async parts creates a new consistency question at the boundary.
Worked example 2: a live score page with 2 million viewers
#A sports site shows a live cricket score. During a big match, 2 million people have the page open. The score changes every 30 seconds or so; commentary lines appear every few seconds. Viewers only read; they never send data. How should updates reach them?
Option A: WebSockets. Real-time, but 2 million persistent two-way connections need a large fleet of connection servers, a fan-out system to push each update to all of them, and careful handling of reconnection storms. The two-way capability is wasted, because viewers never send anything.
Option B: SSE. One-way, simpler than WebSockets, with built-in resume. Still 2 million long-lived connections to hold and fan out to.
Option C: short polling of a cached endpoint. Every client requests GET /matches/123/live every 5 seconds. That is 2,000,000 / 5 = 400,000 requests per second, which sounds alarming. But every viewer asks the same question. Put the endpoint behind a CDN with a 2-second cache lifetime. Each CDN location then fetches from the origin at most once every 2 seconds, no matter how many viewers it serves. With, say, a few hundred edge locations, the origin sees a few hundred requests per second, not 400,000. The CDN, which is built for exactly this, absorbs the rest.
WebSockets | SSE | Cached short polling | |
|---|---|---|---|
Update delay | Under a second | Under a second | Up to about 5 to 7 seconds |
Origin load | 2 million connections | 2 million connections | Hundreds of requests per second |
Complexity | High | Medium | Low |
For a score that changes every 30 seconds, a few seconds of delay is acceptable, and Option C is cheapest and most robust. If the product required sub-second updates (betting odds, for instance), SSE through infrastructure built for streaming would be justified. The requirement decides, not the technology's popularity. This is exactly the kind of reasoning interviewers want to hear.
Trade-offs to state in interviews
#Choice | You gain | You pay |
|---|---|---|
Sync over async | Simplicity, immediate errors, known outcome | Latency and availability coupled to every dependency |
Async over sync | Fast responses, spike absorption, failure isolation | Delay, duplicates, ordering issues, more components, harder debugging |
Polling over push | Stateless, cacheable, simple | Waste and delay |
WebSockets over SSE | Two-way messaging | More complex protocol and scaling |
Push notifications | Reach users when the app is closed | Best-effort delivery, third-party dependency |
Common mistakes
#- Making everything async "for scale". Async adds real complexity. Use it where the decision guide says so.
- Doing slow third-party calls inside the user's request, so the provider's outage becomes yours.
- Async work that is not idempotent, so a redelivered message sends two emails or charges twice.
- No visibility into the background: nobody notices that the queue has been growing for six hours.
- Choosing WebSockets for one-way updates that SSE or cached polling would serve more cheaply.
- Clients that reconnect instantly and in sync after an outage, turning a blip into a storm.
- Forgetting the user experience of delay: show "processing" states, and let users see their own changes immediately.
In an interview
#- For each functional requirement, say which parts are synchronous and which are background work, and why.
- If you introduce a queue, say what it buys (latency, spike absorption, isolation) and what it costs (duplicates, delay).
- For any real-time feature, name the mechanism and justify it with the update frequency, direction and number of clients.
- Mention reconnection with backoff and resume from the last event for any long-lived connection.
Exercises
#Exercise 1
Sync or async?
#For an online learning platform, decide sync or async for each step and give a one-line reason:
- Enrol a student in a free course.
- Generate a PDF certificate when a course is completed.
- Check that a coupon code is valid at checkout.
- Recompute course recommendations after each lesson.
- Send a reminder email to students inactive for 7 days.
Exercise 2: Polling cost
#A delivery app shows the status of active orders. At dinner peak there are 300,000 active orders, and each customer's app polls GET /orders/{id}/status every 3 seconds.
- How many requests per second does that generate?
- Order status changes about 6 times over a 40-minute delivery. Roughly what fraction of polls return a change?
- Can a CDN cache these responses as it did for the live score? Why or why not?
- Suggest a better approach and estimate its load.
Exercise 3: Report completion
#Users of a finance dashboard request a yearly report that takes 30 seconds to 3 minutes to generate. Design how the request is made and how the user learns it is ready, for (a) the web app and (b) a partner company's server that calls your API.
Exercise 4: The crash
#A chat service runs 20 WebSocket servers, each holding about 100,000 connections. One server crashes.
- What happens to those 100,000 users in the next minute if the clients simply reconnect immediately?
- What should the client do instead?
- How do users avoid missing messages sent while they were disconnected?
Show solutionHide solution
- Sync. It is a fast database write, and the student expects to start the course immediately.
- Async. PDF generation is slow and not needed instantly; show "your certificate is being prepared" and notify when ready.
- Sync. The checkout total depends on the answer.
- Async. Recommendations can be minutes stale; recomputing on every lesson inside the request would slow it down for no visible gain.
- Async, scheduled. A periodic job (article 37) finds inactive students and enqueues emails; nobody is waiting on it.
Exercise 2
Show solutionHide solution
- 300,000 / 3 = 100,000 requests per second.
- Over 40 minutes (2,400 seconds) each app polls 2,400 / 3 = 800 times, and about 6 of those see a change: roughly 0.75%. More than 99% of the work returns "no change".
- Not in the same way. Each order's status is private and different, so there is no single shared response for a CDN to cache. Caching per order is possible but gives little benefit because each key is requested by only one client, and private data needs care at a shared cache.
- Push the change instead. Options: an SSE or WebSocket connection while the order screen is open, with mobile push for background notifications. Load becomes one long-lived connection per active screen (at most 300,000 concurrent connections, spread over several servers) plus about 300,000 x 6 = 1.8 million status messages over a 40-minute peak, which averages around 750 messages per second instead of 100,000 polls per second. A compromise that keeps polling but reduces waste: poll every 15 seconds, and use
ETagwithIf-None-Matchso unchanged responses are a tiny304.
Exercise 3
Show solutionHide solution
Both start the same way: POST /v1/reports returns 202 Accepted with Location: /v1/reports/{reportId}. A queue holds the job, and report workers generate the file into object storage and update the report's status.
(a) Web app. Show a "preparing your report" state. Either poll GET /v1/reports/{reportId} every 5 to 10 seconds (simple, and cheap at this scale), or push completion over an existing SSE or WebSocket channel if the app already has one. Also send an email or in-app notification with the download link, because users often leave the page during a 3-minute wait.
(b) Partner server. Let the partner register a webhook URL. When the report is ready, send a signed POST with the report id and a download link (for example a time-limited signed URL, article 10). Retry with backoff if their server is down, include an event id so they can ignore duplicates, and keep the status endpoint available so they can also poll if a webhook is lost.
Exercise 4
Show solutionHide solution
- All 100,000 clients reconnect within the same second. The load balancer sends them to the 19 remaining servers, which each receive over 5,000 new connections at once, each requiring a TLS handshake and authentication. CPU spikes, some handshakes time out, those clients retry immediately too, and the overload can spread until other servers fail. A single crash becomes a cascading outage.
- Reconnect with exponential backoff plus random jitter: wait a random time (for example between 0 and 1 second), then if that fails between 0 and 2 seconds, then 0 and 4, up to a cap. The reconnections spread over many seconds. Servers can also rate-limit new connections and the fleet should keep spare capacity for this case.
- Give every message a per-conversation sequence number, and store messages durably before acknowledging them. On reconnect, the client sends the last sequence number it received for each conversation, and the server sends everything after it, or the client fetches missed messages through a normal paginated API. Article 34 designs this in full.
Where to go next
#- Next: Back-of-the-Envelope Estimation: From Users to Peak QPS gives you the numbers to judge whether a queue, a cache or another server is needed.
- Queues in depth: article 11 explains producers, consumers, acknowledgements, retries and dead-letter queues.
- Duplicates and retries: article 20 makes background work safe to run twice.
- Real-time at scale: the chat (34) and ride-hailing (41) case studies use these mechanisms under heavy load.
Sources & further reading
- Technology
- Software Engineering
- #APIs
- #asynchronous processing
- #beginners
- #distributed systems
- #message queues
- #roadmap
- #scalability
- #system design
- #system design interview
Connected lessons
Linked from 2 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
How to Learn Programming When AI Can Already Write Code
Guide · Technology
Start Here: Your Roadmap from Zero to Job-Ready DSA
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

