Skip to content

    System Design

    Synchronous vs Asynchronous Communication Between Services

    Learn what synchronous and asynchronous communication mean between services, how 202 request-reply, queues and events differ, and how to choose for each call.

    By Published 12 min read

    Share

    Standard lessonbeginnerBackend System Design

    Before this: API Design

    What you'll learn: By the end of this lesson you will be able to say precisely what makes a call synchronous or asynchronous (it is about who waits and when the result returns, not about the protocol or about threads), tell the three asynchronous shapes apart (request then poll, a one-way message, an event), design a 202 Accepted exchange with a status resource, count what a synchronous chain does to latency and availability, and decide per interaction which style to use and what it will cost.

    When you phone a restaurant to ask whether a table is free, you hold the line until someone answers, and you learn the answer in the same breath. When you post a letter asking for a catering quote, you go about your day, and the reply arrives later by another route. Neither is better. The phone call is simple and immediate and fails if nobody picks up. The letter survives a closed office and costs you a delay, a way to receive the answer, and the chance that the letter arrives twice.

    Software works the same way. ShopStream Stage 5 already made checkout asynchronous and gives the sync-or-async test for its steps. This lesson is the general treatment: what the words mean, the shapes asynchronous communication takes, what each costs, and how to choose. For the API side see API design.

    Verified against RFC 9110, RFC 7240 and Microsoft's Asynchronous Request-Reply pattern page, read on 2026-11-08.

    The core idea

    A call is synchronous when the caller waits for the result before it continues, and so both sides must be alive at the same moment. It is asynchronous when the caller hands off the work, continues, and receives the result later by a different path, or never. Everything else follows from that. A synchronous call couples the caller to the callee's speed and availability. An asynchronous exchange breaks that coupling and pays for it with a result that arrives later.

    The words are used loosely, so keep four meanings apart:

    Meaning Example Is the interaction synchronous?
    Non-blocking code inside a process A client that issues an HTTP request and frees its thread while waiting Yes: the caller still needs the answer to proceed
    Request and response on one connection A GET that returns the order Yes
    Request, acknowledgement, result later POST returns 202, the result is read from a status resource No: the work is handed off
    One-way message or event A message on a queue, an OrderDelivered event No: the sender does not wait for any result

    The first row is the common confusion. Microsoft's pattern page notes that application code can make a synchronous API call in a non-blocking way to give the appearance of asynchronous processing, and recommends that for I/O-bound operations. That improves thread use. It does not remove the dependency on the callee's answer.

    Three examples run through the lesson: a minimal example (a chain of three calls, in the next section), a realistic example (ShopStream's order status screen and receipt export, in "In practice") and a failure example (a timeout that hides a request which in fact succeeded).

    What a synchronous call costs

    Take a chain: the app calls the orders service, which calls delivery, which calls the courier service. All numbers below are illustrative.

    sequenceDiagram accTitle: A synchronous chain of three calls accDescr: The app calls orders, orders calls delivery, delivery calls the courier service. Each caller waits. The courier service is slow, so every caller up the chain is slow, and if it is down every caller fails. participant A as App participant O as Orders participant D as Delivery participant C as Courier service A->>O: get order status O->>D: get delivery D->>C: get courier position Note over C: slow or down C-->>D: position, 400 ms later D-->>O: delivery O-->>A: status

    Figure 1. Waiting is contagious. How to read this: each arrow down is a caller blocked until the arrow back. The app's wait includes every wait below it, so one slow callee slows everyone above it.

    • Latency adds. If orders needs 20 ms of its own work, delivery 30 ms and the courier call 400 ms, the app waits at least 450 ms. The slowest link sets the pace.
    • Availability multiplies. If each of the three services is up 99.9 percent of the time and failures are independent, a request that needs all three succeeds about 0.999 x 0.999 x 0.999 = 99.7 percent of the time. Every hop makes the whole worse than its worst part.
    • Timeouts are required. Without one, a hung callee hangs its callers. With one, the caller does not know whether the work happened (see the failure example).
    • Retries amplify. Google's SRE book limits retries to the layer directly above the one that rejected, to three attempts per request, and to a ratio below about 10 percent of a client's requests, because retrying at every layer multiplies load on a struggling service.

    These are the reasons to read resilience patterns next to this lesson. They are also why synchronous is still the right default for most reads: a shopper who opens the order screen needs the answer now, and the simplest thing that gives it is a direct call.

    The shapes of asynchronous communication

    Request, then poll: 202 Accepted

    The client sends a request, gets an immediate acknowledgement, and later reads the result from a status resource. RFC 9110 defines 202 (Accepted) as the request being accepted for processing without the processing having been completed, and says the representation ought to describe the request's current status and point to a status monitor. It also notes that HTTP has no facility for re-sending a status code later from an asynchronous operation, which is why the result must be fetched or pushed by another route.

    Microsoft's Asynchronous Request-Reply pattern fills in the details. The API validates the request first and answers 400 at once if it is invalid; otherwise it returns 202 with a Location header pointing at a status endpoint and a Retry-After header suggesting a polling interval. While the work runs, the status endpoint returns 200 with a status body; when it finishes, it returns 303 (See Other) to the result. The same page advises a retention policy for status resources and an idempotency key on the initial POST.

    POST /exports/receipts HTTP/1.1
    Content-Type: application/json
    
    {"orderId": "ord_1042"}
    
    HTTP/1.1 202 Accepted
    Location: /exports/receipts/exp_77
    Retry-After: 5

    The exchange above is an illustrative protocol example, not captured output. A client may also ask for this behaviour: RFC 7240 defines a respond-async preference, and a server that honours it can answer 202, but the server is free to ignore it.

    sequenceDiagram accTitle: Asynchronous request-reply with a status resource accDescr: The client posts a request and gets 202 with a Location header. A worker does the work from a queue. The client polls the status resource, gets 200 while pending, then 303 pointing to the result. participant C as Client participant A as API participant Q as Queue participant W as Worker C->>A: POST /exports/receipts A->>Q: enqueue job A-->>C: 202, Location, Retry-After W->>Q: take job C->>A: GET status A-->>C: 200 pending W->>A: job done, result stored C->>A: GET status A-->>C: 303 to the result

    Figure 2. Accept now, deliver later. How to read this: the first response only says "I have it". The client's wait is the polling, not the work. Polling is the simple answer when the client cannot receive callbacks; Microsoft's page lists when to prefer webhooks, WebSockets or server-sent events instead.

    A one-way message through a queue

    The sender puts a message on a durable queue and moves on; a worker takes it later. No answer comes back unless a separate message or status carries one. This is the hand-off the order pipeline uses, with the outbox, acknowledgements and dead-letter queues covered in message queues.

    An event

    The sender announces that something happened and does not know or care who listens: OrderDelivered goes to notifications, analytics and loyalty without delivery calling any of them. This removes the sender's dependency on each consumer, and adds an indirect dependency nobody can see from the sender's code. Event streaming covers logs and consumer groups.

    Shape Who waits How the result returns Coupling to the other side
    Request-response The caller, for the whole call In the response Speed and availability
    Request then poll (202) The caller, only for the acknowledgement Status resource, callback A status resource to keep
    One-way message Nobody Another message, or none A broker both sides trust
    Event Nobody Not applicable: no one asked Only the event's meaning

    What asynchronous does not give you

    Asynchronous is not faster. The work takes as long as it takes; the caller just does not stand there. What it gives is independence, and each part of that has a price:

    • The result is eventual. The interface needs a "pending" state, and a way to show "failed later".
    • Messages can arrive twice. Many brokers deliver at least once, so a consumer must be safe to repeat; see idempotent and commutative operations.
    • Order is not free. Two events about one order can arrive out of order unless the design preserves it.
    • A backlog can hide a problem. A queue absorbs a burst, but one that grows without a bound turns overload into delay; see backpressure.
    • Failures show up late. A step that fails ten seconds after the shopper was told "accepted" needs a compensation, not just an error; see distributed transactions.
    • Tracing is harder. One request becomes several records in several places, so a correlation id must travel with the work.
    flowchart TD accTitle: Choosing synchronous or asynchronous for one interaction accDescr: If the caller needs the answer to continue, call synchronously with a deadline and a fallback. Otherwise, if the work is long or the callee may be down, use a one-way message or request-then-poll. If many independent parties care, publish an event. S["One interaction between services"] --> Q1{"Does the caller need the result to continue?"} Q1 -->|"yes"| SY["Synchronous call with a deadline and a fallback"] Q1 -->|"no"| Q2{"Do many independent parties react to it?"} Q2 -->|"yes"| EV["Publish an event"] Q2 -->|"no"| Q3{"Does the requester need to learn the outcome later?"} Q3 -->|"yes"| PO["Request then poll, or a callback"] Q3 -->|"no"| MS["One-way message to a queue"]

    Figure 3. A decision per interaction. How to read this: ask it for each call, not once for the system. The first question separates the two worlds; the next three pick the shape. Most systems mix them: a synchronous front door and asynchronous work behind it.

    The other side of this topic

    On the front end this is the difference between a screen that waits for an answer and one that shows "working on it". The user interface has to model the pending state, decide how often to poll or whether to listen for a push, and recover when the later result is a failure. The Frontend System Design track covers consuming APIs from the client; here the rule is that the server must give the client something it can act on: a status resource, a clear error, a retry hint.

    In practice: the order screen and the receipt export

    This is the realistic example. It is an educational scenario on the course's running example, ShopStream, and nothing in it describes a real company. Every figure is illustrative.

    This is a realistic scenario, not a case history.

    The situation

    After Stage 5, checkout is asynchronous. Two new features arrive. The order screen shows status, courier position and receipt link, drawing on three services. A "download receipts" button builds a PDF of a month's orders, which can take tens of seconds. The team asks which of these should be synchronous. The browsing load is the course's 2,300 requests per second at peak; the export is rare.

    Starting architecture

    The order screen and the receipt export, side by sideShopper app to API gateway; API gateway to Orders (GET status); Orders to Delivery (deadline 300 ms); API gateway to Export API (POST, then poll); Export API to Job queue; Job queue to PDF workerSYNCHRONOUS: NEEDED NOWASYNCHRONOUS: TAKES TIMEShopper appShopper appAPI gatewayAPI gatewayOrders: status readOrdersstatus readDelivery: courier positionDeliverycourier positionExport API: 202 and statusExport API202 and statusJob queueJob queuePDF workerPDF workerGET statusdeadline 300 msPOST, then poll

    Figure 4. Two interactions, two styles. How to read this: the top path is a read the shopper is looking at, so it stays synchronous with a deadline. The bottom path hands work to a queue and returns a status link.

    Options considered

    Option Behaviour Cost Risk
    A. Both synchronous Simplest One slow export holds a connection and a thread for its whole duration Gateway timeouts, retried exports
    B. Both asynchronous Uniform The order screen shows "loading" and a poll for a read Complexity where a direct call was enough
    C. Read synchronous, export request-then-poll Each fits its need Two patterns to operate A status resource to keep and expire

    The decision

    Option C. The order screen calls orders synchronously with a 300 ms deadline for the courier position (an illustrative figure): if delivery is slow, the screen shows status without the position, instead of failing. The export answers 202 with a Location and Retry-After, validates the request first, and requires an idempotency key so a retried POST returns the existing job. Status resources expire after a retention period.

    Rollout and verification

    Ship the export behind a flag. Verify that an invalid request gets 400 and no job, that a duplicate key returns the same status resource, that the status moves from pending to done, and that an expired status returns a clear answer. Watch three signals: export job age at the head of the queue, the share of order-screen requests that used the fallback, and the number of exports retried.

    The 2 a.m. failure

    The page reads: duplicate receipt exports and a worker backlog. A load balancer timed out the first POST before the response returned, the app retried, and without an idempotency key the API created a second job each time. The loop is the failure example below, now amplified by retries from many shoppers. The checks: compare job ids per order and look at the age of the oldest job. The mitigation is to deduplicate on the key and drop queued duplicates; the lasting fix is the idempotency key on every POST that creates work.

    sequenceDiagram accTitle: A timeout that hides a request which succeeded accDescr: The client sends a POST, the server creates the job, but the response is lost or too slow and the client times out. The client cannot tell this from a request that never arrived, so it retries and the server creates a second job unless an idempotency key identifies the retry. participant C as Client participant S as Server C->>S: POST create job S->>S: job 1 created S--xC: response lost or too slow Note over C: timeout: did the server get it? C->>S: POST create job again S->>S: job 2 created, unless the key shows it is a retry S-->>C: 202

    Figure 5. The question a timeout cannot answer. How to read this: the client sees only silence. RFC 9110 says a client should not automatically retry a request with a non-idempotent method, and POST is not among the methods it defines as idempotent, so the retry is safe only because of the key.

    Architect's notes

    • Default to synchronous for reads the user is looking at. Add a deadline and a fallback.
    • Go asynchronous where the work is slow, third-party, or must survive a callee being down. Say what the user sees while it is pending.
    • Make every request that creates work safe to repeat. Idempotency keys cost little and remove a class of incidents.
    • Pick the result channel on purpose. Poll when the client cannot receive callbacks; push when latency matters and a channel exists.
    • Measure the right things. Latency percentiles and fallbacks for synchronous calls; age of the oldest message and failures per step for asynchronous ones.
    • Mix freely, but keep the boundary clear. A synchronous front door with asynchronous work behind it is normal.

    Common mistakes

    • Calling a non-blocking client "asynchronous". The interaction is still synchronous.
    • Chains of synchronous calls with no deadlines. One slow service stalls everything above it.
    • Retrying a POST without an idempotency key.
    • Returning 202 and never giving the client a way to learn the outcome.
    • Making everything asynchronous. A read the user waits for gains nothing from a queue.
    • Events that hide a coupling. If the sender needs a specific consumer to act, that is a command, not an event.

    Key takeaways

    • Synchronous means the caller waits for the result; asynchronous means the work is handed off and the result comes later or never.
    • Non-blocking code in a client does not make an interaction asynchronous.
    • Synchronous chains add latency and multiply failure probability, so each hop needs a deadline and a fallback.
    • Asynchronous has three shapes: request then poll, one-way message, event.
    • 202 Accepted means accepted, not done; it needs a status resource, Location and ideally Retry-After.
    • Asynchronous costs eventual results, duplicates, ordering care and harder tracing.
    • Choose per interaction, and make requests that create work idempotent.

    Practice exercises

    Exercise 1 (20 min). For a chain of five synchronous services, each up 99.95 percent of the time (illustrative), compute the availability of a request that needs all five.

    Exercise 2 (30 min). Classify each ShopStream interaction as request-response, request-then-poll, one-way message or event: add to cart, place order, courier position on the map, "order delivered" notification, monthly receipt export.

    Exercise 3 (45 min). Write the 202 response, the pending and done status bodies and the 303 for the receipt export, including headers.

    Exercise 4 (1 hour). Draw Figure 5 for a payment request and add the idempotency key. What does the server return for the retry?

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments