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.
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.
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
OrderPlacedevent 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 |
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 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:
- The client POSTs the job. The API validates first and returns 400 for a bad request while the client is still listening.
- The API stores a job record, enqueues the work and returns 202 Accepted with
Location(the status URL) andRetry-After(when to poll). - The status endpoint returns 200 with a status body until the job ends, then 303 See Other pointing at the result.
- The client fetches the result. A
DELETEon 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.
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.
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 |
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:
- Does the caller need the result to take its next step? Then that step is synchronous.
- What if the downstream is down for 10 minutes? If “the work can wait”, go asynchronous.
- Is the downstream slower, flakier or burstier than the caller? Put a queue between them.
- How many parties need to know? One that must act gets a command; many that react get an event.
- Can the work safely run twice or late? Then asynchronous retries are safe; if not, make it idempotent first.
- Does it take more than a few seconds? Use 202 plus a status resource, or a callback.
- 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 |
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
OrderPlacedoutbox 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
- Message queues and publish/subscribe: the two shapes of asynchronous delivery.
- Delivery guarantees and DLQs: why consumers must be idempotent.
- Transactional outbox: an event and a database write, safely.
- Two-phase commit vs sagas: undoing work after a late failure.
- Timeouts, retries and backoff with jitter and circuit breaker: safe synchronous calls.
- Backpressure and load shedding: more load than either style can take.
- Event-driven architecture, push vs pull and stateful vs stateless design: the neighbouring ideas.
References and further reading
- 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.
- 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.
- 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.
- 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.
- 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.
- gRPC guide: Deadlinesgrpc.iogRPC sets no deadline by default, DEADLINE_EXCEEDED, server-side cancellation and deadline propagation as a shrinking timeout.
- 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.
- 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.
- 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.
- 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.