Sync vs async
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 18 min · 5 figures

    Synchronous vs asynchronous communication

    When one service should call another and wait for the answer, when it should hand off a message and move on, what each style costs in latency, availability and complexity, and the hybrid most real systems use.

    Synchronous communication means the caller sends a request and waits, up to a timeout, for the answer, as in an HTTP or gRPC call. Asynchronous communication means the caller hands off a message, event or job and carries on, and any result arrives later through another channel. Synchronous gives you an answer now but ties your fate to the service you call. Asynchronous decouples the two in time, and you pay with results that arrive later, duplicate messages and harder debugging.

    Picture it first: the dosa counter and the token board

    A food court offers two ways to get dinner.

    At the dosa counter you order and stand there while the cook makes it. If the cook is slow, so are you; if the counter is shut, you find out at once. It works like a phone call: an answer, or a clear “no”, right now.

    At the biryani stall you pay, get token 47 and sit down. The kitchen works at its own pace; in a rush, the token board just grows longer. When 47 flashes you collect your plate. If the kitchen stops for ten minutes to change a gas cylinder, your order is late, not lost. It works like a WhatsApp message: send it and get on with your day.

    The token has costs. You don’t know when the food will come, you must watch the board, and if the kitchen runs out of mutton after taking your money, someone has to find you and refund it.

    Map it back:

    • Standing at the counter is synchronous request/response; the cook is the service you call; your patience is the timeout.
    • The token is a message with an ID, handed to a broker (the kitchen’s order rail). The board is a status endpoint or a push notification.
    • The kitchen working steadily through the rush is load levelling; the gas-cylinder pause is a consumer outage the caller never notices.
    • The refund hunt is a compensation: with asynchronous work, failures surface after the caller has left.

    The two styles, precisely

    Synchronous: the caller sends a request and waits on the same connection for the response, up to a timeout or deadline. REST calls, gRPC unary calls and database queries all work this way (see REST vs GraphQL vs gRPC). The defining property is temporal coupling: in microservices.io’s words, client and service must both be available for the duration of the request.

    Asynchronous: the caller hands a message to an intermediary, usually a message queue or a pub/sub topic. It gets a quick acknowledgement that the message is stored, and moves on. The consumer processes it whenever it is ready. A reply, if there is one, comes back on a different channel: a reply queue, a webhook or a status the caller checks.

    The same piece of work done synchronously (left) and asynchronously (right).Notice Where the waiting happens. On the left the caller is blocked for the whole call. On the right it is blocked only for the broker’s acknowledgement, and the consumer can run minutes later.

    Name the kind of message you send:

    • Command: “do this”, sent to one recipient expected to act, such as GenerateInvoice.
    • Event: “this happened”, published for whoever cares, such as OrderPlaced. Martin Fowler warns against the passive-aggressive command, an event the sender secretly expects someone to act on. If you need it done, send a command.
    • Notification (fire-and-forget): no reply expected. Request with an asynchronous response: a reply expected, eventually.

    Not the same as async I/O

    “Async” also describes how code waits inside one process: await, event loops, non-blocking sockets. A service that awaits an HTTP call without blocking a thread is still in a synchronous conversation: its user waits, and if the callee is down the request fails. Non-blocking I/O saves threads; it doesn’t decouple services in time. For that axis, see concurrency vs parallelism and the synchronous I/O anti-pattern.

    What synchronous buys you

    • A simple mental model. A remote call reads like a function call, and one trace shows the whole path.
    • An answer now. “Payment authorised”, “seat confirmed”, “wrong OTP”: the user acts on it immediately.
    • Read-your-writes for free. When the call returns, the work is done.
    • Natural error handling. A 409 Conflict comes straight back to the one place that can deal with it.
    • No broker. One less system to run, and for a short call nothing beats a direct round trip. Azure notes that most APIs respond in about 100 ms or less.

    What synchronous costs

    Availability multiplies

    If a request needs every service in a chain, it succeeds only when all of them are up. With each at 99.9%:

    • 3 services: 0.999³ ≈ 99.70%
    • 5 services: 0.999⁵ ≈ 99.50%
    • 10 services: 0.999¹⁰ ≈ 99.00%

    A 30-day month has 43,200 minutes. One service at 99.9% fails for about 43 of them; a 10-service chain fails for 0.995% × 43,200 ≈ 430 minutes, over 7 hours. Nobody chose to run a 99% service; you get one by adding calls (see availability and the nines). The product assumes independent failures; correlated ones shift the number, not the direction.

    Latency adds up, and the tail dominates

    Sequential calls add: six calls at 80 ms each take 480 ms. Parallel calls take as long as the slowest one, and that is where the tail bites. Jeff Dean and Luiz André Barroso’s example in “The Tail at Scale”: a server usually answers in 10 ms but takes 1 s at p99. A request served by one such server is slow 1 time in 100. Fan out to 100 of them and wait for all, and 1 − 0.99¹⁰⁰ ≈ 63% of requests take over a second. In a real Google service they measured, one leaf answered within 10 ms at p99, but waiting for all leaves took 140 ms at p99.

    Two kinds of multiplication in synchronous designs. Left: monthly downtime of a chain whose services are each 99.9% available. Right: the share of requests that hit at least one slow (p99) call as the number of calls grows.Notice Neither chart involves any service getting worse. Each component is exactly as good as before; the request is worse only because it depends on more of them at once.

    Waiting holds resources

    A blocked caller keeps a thread, a connection and memory. Little’s law gives in-flight requests = arrival rate × time each one takes. At 1,000 requests a second and 80 ms per call, about 80 are in flight. Now the dependency hangs and every call runs to a 2-second timeout: 1,000 × 2 = 2,000 in flight. A pool of 200 threads can then finish at most 200 ÷ 2 = 100 requests a second, and the other 900 queue or fail.

    The slow service has made its caller slow, and the caller’s callers are next: a cascading failure. That is why Amazon’s Builders’ Library puts a timeout on every remote call.

    Retries multiply

    Retries rescue transient errors, but stacked they multiply. Amazon’s example is a five-deep call stack where each layer tries three times: when the database at the bottom starts failing, it receives 3⁵ = 243× the load. Retry at one layer only, with capped exponential backoff, jitter and a retry budget, and put a circuit breaker in front of a dead dependency so callers fail fast. See timeouts, retries and backoff with jitter.

    Scaling is coupled

    The caller’s peak is the callee’s peak. If checkout gets 5× traffic on a sale day, every service it calls synchronously must absorb 5× in the same second, or shed it.

    What asynchronous buys you

    • Temporal decoupling. The consumer can be down, deploying or slow; the broker keeps the message. microservices.io counts improved availability among messaging’s benefits.
    • Load levelling. The queue soaks up a spike and consumers drain it steadily, so you size for average load, not peak.
    • Independent scaling. The API scales on requests, the workers on backlog.
    • Cheap fan-out. One OrderPlaced event reaches every interested service, and adding a consumer changes no producer.
    • Retries nobody waits for, and a faster response, because slow work is off the critical path.

    Load levelling in numbers: a Big Billion Days-style sale sends 5,000 orders a second for 10 minutes to invoice workers that handle 1,000 a second. The backlog grows by (5,000 − 1,000) × 600 = 2.4 million invoices. When orders fall to 500 a second, the workers have 500 a second spare, so draining takes 2.4M ÷ 500 = 4,800 s, about 80 minutes. Invoices arrive late, but no checkout failed. A synchronous design would need five times the invoice capacity, or would fail four checkouts in five.

    What asynchronous costs

    • Eventual results. The caller learns “accepted”, not “done” (see strong vs eventual consistency).
    • Slower end to end. The broker hop, queue wait and maybe an outbox relay’s polling interval add up.
    • Duplicates and reordering. Most brokers deliver at least once, and parallel consumers reorder (see delivery guarantees and message ordering).
    • An invisible flow. Fowler notes that with event notification the flow is not explicit in any program text; often only the live system shows it. You need a correlation ID on every message and distributed tracing.
    • Failure handling moves. No exception reaches the caller: poison messages go to a dead-letter queue, and business failures need compensating actions.
    • More infrastructure. The broker must be highly available and monitored; alarm on the age of the oldest message.
    • Pending states in the UI. “Waiting for the restaurant” screens, polling or push all need building.

    Async doesn’t make the work faster

    It makes the response faster by not waiting for the work. If the user needs the result to continue, a queue adds a broker hop, queueing delay and a polling loop to their wait.

    Synchronous Asynchronous
    Caller gets The result or an error, now An acknowledgement; the result later
    Coupling in time Both must be up at once Consumer can be down while messages are retained
    Availability over a chain Product of the links: 0.999⁵ ≈ 99.5% Roughly the caller’s own, plus the broker if published inline
    Latency the user sees Sum of the chain, or the slowest call of a fan-out Just the hand-off, a few milliseconds
    End-to-end latency Lowest Higher: broker hop plus queue wait
    Traffic spikes Callee must absorb them now Queue absorbs them; workers drain later
    Consistency Done when it returns Eventual
    Errors Returned to the caller Retries, DLQ, compensations
    Typical tech REST, gRPC unary, GraphQL SQS, RabbitMQ, Kafka, Pub/Sub, webhooks
    The two styles side by side.

    Worked example: placing a food-delivery order

    A user taps “Place order” in a Swiggy- or Zomato-style app. For each step, ask one question: does the user need this step’s result before they see “Order placed”?

    Step Needed before “Order placed”? Style
    Validate cart, price and coupon Yes: the total must be right Synchronous
    Authorise the payment Yes: no order without money Synchronous, with an idempotency key
    Write the order Yes: the order ID is the answer Synchronous, one local transaction
    Tell the restaurant No: accepting takes time anyway Event
    Assign a rider No: matching can take minutes Event
    SMS, push and email No Event
    Loyalty points, analytics, invoice PDF No Event

    Synchronous everything. The order service calls pricing, payment, restaurant, rider, notifications and loyalty one after another at about 80 ms each: 6 × 80 + 10 ms for its own write = 490 ms. Seven components (the order service plus six) must all be up: 0.999⁷ ≈ 99.30%, about 300 minutes of failed checkouts a month. And a restaurant whose staff take a minute to tap “accept” can’t sit inside an HTTP request at all.

    Hybrid. Pricing and payment stay synchronous. Then one local transaction writes the order and an OrderPlaced outbox row: 80 + 80 + 10 = 170 ms, about 2.9× faster. Three components on the critical path give 0.999³ ≈ 99.70%, about 130 minutes a month, so 170 fewer minutes of failed checkouts. A relay publishes the event, and the consumers react at their own pace.

    The hybrid order flow. Three synchronous steps sit on the critical path; everything else hangs off one event. The bars underneath compare the time the user waits with the synchronous-everything design.Notice The broker is not on the critical path at all. The event is written in the same transaction as the order, so a broker outage delays notifications but never fails a checkout.

    The outbox takes the broker off the critical path

    If the order service published to Kafka inside the request, the broker would become another synchronous dependency, and a dual write: commit, crash before publishing, and the restaurant never hears of the order. An outbox row written in the same transaction makes the event part of the order; a relay publishes it later. See the transactional outbox.

    What if something fails after the reply? If no rider is found within 10 minutes, the rider service publishes RiderUnavailable and the order is cancelled and refunded. That compensation makes the flow a saga, driven by events or by an orchestrator (see two-phase commit vs sagas); with many steps and timers, a workflow engine keeps it readable.

    Even the payment step may be asynchronous underneath, if the payer approves in a separate app and the result arrives through a callback. The user’s screen still waits, with a timeout, for a definite yes or no: synchronous describes what the caller experiences, not every hop beneath it.

    A second pair: tatkal booking vs a year’s statement

    Tatkal booking: synchronous, with a safety net. The moment tatkal booking opens, thousands race for the same berths, and each needs a definite PNR or a definite “not booked”. The booking call is synchronous with a deadline and carries an idempotency key. If it times out, the booking may still have succeeded, so the client must not blindly resubmit. It asks “what happened to key K?” and shows “Confirming your booking…” until it knows. The berth itself is protected by compare-and-set.

    A 12-month bank statement, or a video transcode: asynchronous request-reply. The work takes longer than any load balancer will hold a connection open. Azure’s Asynchronous Request-Reply pattern is the standard shape:

    1. The client POSTs the job. The API validates first and returns 400 for a bad request while the client is still listening.
    2. The API stores a job record, enqueues the work and returns 202 Accepted with Location (the status URL) and Retry-After (when to poll).
    3. The status endpoint returns 200 with a status body until the job ends, then 303 See Other pointing at the result.
    4. The client fetches the result. A DELETE on the status resource can cancel the job, and results expire after a retention period.

    RFC 9110 explains why the status resource must exist: 202 is “intentionally noncommittal”, and HTTP has no way to send a status code later. Azure also recommends an Idempotency-Key on the POST, since a client can’t tell a lost 202 from a request that never arrived.

    Asynchronous request-reply over HTTP: accept fast, poll a status resource, then follow a 303 to the result.Notice Every individual HTTP exchange is short and synchronous. The long job lives between them, tracked by a job ID instead of an open connection.

    Polling works through any firewall. When the client is a server with a public endpoint, a webhook removes the polling; for a browser, server-sent events or WebSockets push the result (real-time connections). Push or pull is its own trade-off (push vs pull architecture), and long-running jobs covers the worker side.

    Request-reply over a message broker

    Sometimes you need a reply but want a broker’s routing and buffering, say for a pricing engine behind a queue. Enterprise Integration Patterns calls this Request-Reply: the request goes on one channel and the reply returns on another, almost always point-to-point. The request carries a return address (AMQP’s reply_to) and a unique ID, which the replier copies into the reply as the correlation ID.

    RabbitMQ’s RPC tutorial adds the practical rules: one callback queue per client, not per request, and discard replies with an unknown correlation ID. A replier that crashes after replying but before acknowledging will process the request again and reply twice, so the operation should be idempotent.

    Request-reply over a broker. The request names its reply queue and carries a correlation ID; the reply carries the same ID back.Notice Two outstanding requests (c-81 and c-82) share one reply queue. The correlation ID is the only thing that tells their replies apart.

    The requestor can block a thread until the reply arrives, or register a callback and let one listener thread handle all replies: EIP’s two options. Either way, pending requests live in the requestor’s memory, which makes it stateful: restart it and in-flight replies are orphaned. And if the caller still waits with a timeout, it is still coupled in time. You bought buffering, load balancing and routing, not availability, which is why RabbitMQ’s own advice is: when in doubt, avoid RPC and use an asynchronous pipeline.

    The safety kit for each style

    Risk Synchronous defence Asynchronous defence
    Waiting forever A deadline on every call, propagated downstream Message expiry; alarm on the age of the oldest message
    Transient failure Retry at one layer: capped backoff, jitter, retry budget Broker redelivery with backoff and a max attempt count
    Duplicates from retries Idempotency key on every write Idempotent consumer or inbox
    Dead dependency Circuit breaker and a fallback Messages wait; poison messages go to a DLQ
    Overload Load shedding, bulkheads Queue absorbs it; scale on backlog; slow the producers
    Lost work between DB and broker Not applicable Transactional outbox
    Failure after several steps Avoid long chains Saga with compensations
    Debugging Request ID in headers, tracing Correlation ID on every message, tracing
    Slow tail Hedged requests for idempotent reads Not on the user’s path
    Each style fails in its own way. The usual defences side by side.

    Deadlines. gRPC sets no deadline by default, so a client can wait effectively forever. Set one and let it shrink as it travels: if the client allows 2 s and the first service spends 0.5 s, the next call gets the remaining 1.5 s. Amazon picks timeouts from the dependency’s latency: accept, say, a 0.1% false-timeout rate and use the downstream p99.9.

    Hedged requests. For idempotent reads, send a second copy to another replica if the first is slow. In Dean and Barroso’s BigTable benchmark (1,000 keys over 100 servers), hedging after 10 ms cut the p99.9 from 1,800 ms to 74 ms for 2% more requests.

    Choosing: the decision guide

    Ask these questions for every interaction, not once for the whole system:

    1. Does the caller need the result to take its next step? Then that step is synchronous.
    2. What if the downstream is down for 10 minutes? If “the work can wait”, go asynchronous.
    3. Is the downstream slower, flakier or burstier than the caller? Put a queue between them.
    4. How many parties need to know? One that must act gets a command; many that react get an event.
    5. Can the work safely run twice or late? Then asynchronous retries are safe; if not, make it idempotent first.
    6. Does it take more than a few seconds? Use 202 plus a status resource, or a callback.
    7. Will the user notice staleness or ordering? Write synchronously; send only side effects through events, partitioned by key.
    If… Pick Example
    The user can’t proceed without the answer Synchronous, deadline plus idempotency key UPI payment confirmation, tatkal booking
    The page renders the data now Synchronous read, cached Product page, search results, account balance
    Bad input must be rejected now Synchronous validation OTP check, coupon validity, pincode serviceability
    It’s a side effect nobody waits for Asynchronous event Receipt email, SMS, loyalty points
    Many services react to one fact Pub/sub event OrderPlaced to restaurant, rider, analytics
    The downstream is slow, flaky or rate-limited Queue SMS gateway, partner API, invoice PDF
    Spikes far exceed consumer capacity Queue for load levelling Sale-day confirmations, clickstream ingestion
    The work takes seconds to hours 202 plus status, or webhook Video transcoding, statement export, batch scoring
    A partner must hear when something finishes Webhook callback Payment gateway notifying a merchant
    A business process spans services and can fail midway Saga or workflow engine Order, payment, delivery and refunds
    If… → pick… → example.

    Pick synchronous for UPI payment confirmation, login and OTP verification, tatkal seat booking, “do you deliver to my pincode?”, the product page and the bank balance screen. Pick asynchronous for the receipt email, the invoice PDF, notifying the restaurant, updating the search index, recomputing recommendations, clickstream analytics and pushing a cricket score update to millions of phones.

    The hybrid most systems use is a short synchronous spine with asynchronous side effects. Reads and the core write are synchronous; the write records an event in the same transaction; everything else subscribes, and the user sees progress through push or polling. This is event-driven architecture applied only where it pays.

    In the interview

    Phrase the choice per interaction, with its price: “Payment authorisation stays synchronous, because the user can’t get an order ID without it. I pay for that with a hard dependency, so it gets a 2-second deadline, one retry and an idempotency key. Notifying the restaurant is an event, because nobody waits on it. I pay with eventual delivery and duplicate messages, which is fine because the consumer deduplicates on the order ID.”

    Going deeper: traps interviewers listen for

    “Make everything async with Kafka.” The user still needs an answer, so you rebuild request-reply on the log with correlation IDs, reply topics and timeouts: synchronous coupling plus a broker, duplicates and ordering questions. Use a broker where it buys decoupling, load levelling or fan-out.

    “Async means faster.” End to end it is usually slower; only the caller’s response gets faster.

    Forgetting the pending state. Once a step is asynchronous, the UI must show “processing”, survive a refresh and explain failures. Returning 202 and forgetting the client is half a design.

    Six-deep synchronous chains. Every service needs all the others up: a distributed monolith with network latency. Call in parallel, or remove calls with event-carried state transfer, Fowler’s term for keeping a local, event-fed copy of data you would otherwise fetch. You gain resilience and latency and pay with stale copies.

    A synchronous call hidden inside a consumer. The consumer inherits that API’s availability and its backlog grows. Azure warns that autoscaling consumers without bounding their combined rate just moves the overload downstream.

    A queue in front of a service that is always too slow. If producers outpace consumers on average, the queue grows without bound. A queue absorbs peaks; it doesn’t add capacity.

    Cheat sheet

    Idea Remember it as
    Synchronous Caller waits on the same connection, up to a deadline
    Asynchronous Caller hands off and moves on; any reply comes later on another channel
    Not the same as Async I/O inside one process: that saves threads, not coupling
    Chain availability Product: 0.999⁵ ≈ 99.5%, 0.999¹⁰ ≈ 99.0% (about 7 hours a month)
    Fan-out tail 1 − 0.99¹⁰⁰ ≈ 63% of requests hit a p99
    Blocked callers In flight = rate × time: 1,000/s × 2 s = 2,000
    Stacked retries 3 tries at each of 5 layers: 3⁵ = 243× load
    Async buys Temporal decoupling, load levelling, fan-out, retries nobody waits on
    Async costs Eventual results, duplicates, hidden flow, a broker, pending-state UX
    Bridges 202 + Location + Retry-After; webhooks; reply_to + correlation ID
    Default design Short synchronous spine, outbox event, asynchronous side effects

    How to say it in the interview

    “I decide per interaction. If the caller needs the result to take its next step, the call is synchronous, with a deadline, one retry with jitter, an idempotency key and a circuit breaker. Everything else is asynchronous. For an order, pricing and payment are synchronous, and the order is written in one transaction with an OrderPlaced outbox row, so the user waits about 170 ms on three components instead of 490 ms on seven. Restaurant, rider, notifications and loyalty consume that event with idempotent handlers and a DLQ. Anything slower than a few seconds becomes a 202 with a status resource.”

    Why not make payment asynchronous too? The user can’t get an order ID until money is secured, so they would wait anyway, now behind a queue and a polling loop. If the payment provider itself answers asynchronously, I’d hide that behind a short synchronous wait with a timeout and a status check by idempotency key.

    The payment call timed out. Did the payment happen? Unknown, and that’s the point. Never blindly retry a non-idempotent write. Retry with the same idempotency key, or query the payment’s status by that key, and show “confirming” until it resolves.

    How does the user find out the restaurant accepted? The restaurant service publishes OrderAccepted. A notification service pushes it to the app over a WebSocket or a mobile push, and the app can also poll the order’s status as a fallback.

    Isn’t request-reply over a queue as good as HTTP, and more resilient? It buffers and load-balances, but the caller still waits with a timeout, so it is still coupled in time, and you add a broker, reply queues and correlation logic. I’d use it only when the replier is reachable only through messaging.

    Where to go next

    References and further reading

    1. Pattern: Messaging (microservices.io)microservices.ioSynchronous calls need both sides available for the whole request; the five asynchronous interaction styles; a broker improves availability but must itself be highly available.
    2. Asynchronous Request-Reply pattern (Azure Architecture Center)learn.microsoft.com202 Accepted with Location and Retry-After, a status endpoint, 303 See Other on completion, validation first, idempotency keys, cancellation and result retention.
    3. Queue-Based Load Leveling pattern (Azure Architecture Center)learn.microsoft.comA queue as a buffer so a service is sized for average rather than peak load; queues are one-way; a queue grows without bound if producers outpace consumers on average.
    4. The Tail at Scale (Dean and Barroso, Communications of the ACM, 2013)cacm.acm.orgFan-out to 100 servers makes 63% of requests hit a one-in-a-hundred slow response; measured fan-out tails at Google; hedged requests cutting p99.9 from 1,800 ms to 74 ms.
    5. Timeouts, retries, and backoff with jitter (Amazon Builders’ Library)aws.amazon.comTimeouts on every remote call, choosing a timeout from downstream p99.9, the 243× load of retries at five layers, retrying at one point, jitter and idempotent APIs.
    6. gRPC guide: Deadlinesgrpc.iogRPC sets no deadline by default, DEADLINE_EXCEEDED, server-side cancellation and deadline propagation as a shrinking timeout.
    7. RFC 9110: HTTP Semantics (202 Accepted, 303 See Other, Retry-After)rfc-editor.org202 is intentionally noncommittal and HTTP cannot resend a status later; 303 points to a separate result resource; Retry-After in seconds or as a date.
    8. Request-Reply (Enterprise Integration Patterns, Hohpe and Woolf)enterpriseintegrationpatterns.comRequest and reply on separate channels, point-to-point replies, and the synchronous-block versus asynchronous-callback requestor; links to Return Address and Correlation Identifier.
    9. RabbitMQ tutorial 6: Remote procedure callrabbitmq.comreply_to and correlation_id, one callback queue per client, discarding unknown correlation IDs, duplicate replies, and the advice to prefer an asynchronous pipeline.
    10. What do you mean by “Event-Driven”? (Martin Fowler, 2017)martinfowler.comEvent notification and its hidden flow, the passive-aggressive command, and event-carried state transfer as a way to avoid calling the source system.