Back to blog
Synchronous vs asynchronous communication: request-response, fire-and-forget, and event-driven patterns
System Design

Synchronous vs Asynchronous Communication Explained

Aug 8, 2026 8 min read Avinash Tyagi
synchronous vs asynchronous communication event driven architecture fire and forget request response message queue microservices communication event driven microservices distributed systems system design asynchronous messaging

Every distributed system makes one decision over and over, usually without writing it down. When service A needs something from service B, does A wait for the answer, or move on? That single choice, synchronous versus asynchronous communication, quietly shapes your latency, your failure behavior, your coupling, and how hard your 3 a.m. on-call shift will be.

Most engineers reach for the default they learned first, a blocking HTTP call, and never revisit it. Then traffic grows, one slow dependency stalls a whole request chain, and the postmortem reads like a horror story. This post breaks down the three patterns hiding behind that decision: request-response, fire-and-forget, and event-driven. You will see when each fits, what each costs, and how to pick deliberately.

The core split: synchronous vs asynchronous communication

The difference between synchronous and asynchronous communication is not about threads or async/await syntax. It is about whether the caller's progress depends on the callee's response.

In synchronous communication, the caller blocks until it gets a reply. The two services are coupled in time. If B is slow, A is slow. If B is down, A fails.

In asynchronous communication, the caller hands off the work and continues. The reply, if any, arrives later through a callback, a queue, or an event. A and B are decoupled in time, so B can be slow, restarting, or briefly offline while A keeps serving traffic.

That temporal coupling is the whole game. Latency budgets, retry logic, and ordering guarantees all follow from it.

Pattern 1: Request-response (synchronous)

Request-response is the pattern you already know. The client sends a request and blocks until the server returns a result. REST over HTTP, gRPC unary calls, and a plain database query are all request-response.

request_response.pypython
# request_response.py
import requests

def get_user_profile(user_id: str) -> dict:
    # Caller blocks here until payment-service replies
    resp = requests.get(
        f"https://payment-service/users/{user_id}/balance",
        timeout=2,  # always bound the wait
    )
    resp.raise_for_status()
    return resp.json()

It shines when the caller cannot proceed without the answer: reading a balance before rendering checkout, validating a token before granting access, fetching a product before showing its price. These are correctness-first paths where staleness is unacceptable.

The cost is temporal coupling. Worst-case latency is the sum of every synchronous hop, and availability is the product of every dependency's availability. Chain five services at 99.9 percent each and your ceiling is already near 99.5 percent. This is why deep synchronous chains are fragile, and why an API gateway in front of them becomes a single failure domain worth designing carefully, as covered in our guide on API gateway best practices.

Pattern 2: Fire-and-forget (asynchronous, no reply)

Fire-and-forget flips the contract. The caller sends work and does not wait for a result, or even confirmation that it succeeded. The call returns as soon as the message is accepted.

fire_and_forget.pypython
# fire_and_forget.py
def emit_signup_event(user_id: str, channel) -> None:
    # Publish and move on. No reply, no blocking.
    channel.basic_publish(
        exchange="events",
        routing_key="user.signup",
        body=user_id,
    )

It fits work the user should not wait on: sending a welcome email, writing an analytics event, warming a cache, rendering a thumbnail. The user signed up successfully; whether the email goes out in 50 milliseconds or 5 seconds does not change their experience.

The benefit is responsiveness and decoupling. Your signup endpoint returns fast because it is not blocked on an email provider. The email service can restart or fall behind and catch up without ever touching signup latency.

The cost is uncertainty. You gave up the reply, so you gave up knowing, in the moment, whether the work happened. Fire-and-forget needs a durable transport, usually a message queue, plus retries and a dead-letter queue. Without durability, "fire-and-forget" quietly becomes "fire-and-lose." A durable broker absorbs these bursts, which is how many teams scale a boring, reliable stack, as in this look at scaling Postgres, Django, and RabbitMQ to billions.

Pattern 3: Event-driven (asynchronous, reactive)

Event-driven communication looks like fire-and-forget on the surface, but the intent differs. In fire-and-forget, the producer delegates a known task. In an event-driven architecture, the producer announces that something happened and does not know or care who reacts.

event_driven.pypython
# event_driven.py
# Producer announces a fact, names no consumer
def on_order_placed(order, bus):
    bus.publish("OrderPlaced", {
        "order_id": order.id,
        "user_id": order.user_id,
        "total": order.total,
    })
# Consumers subscribe independently: inventory, email, analytics.
# The producer is unaware any of them exist.
Comparison of request-response, fire-and-forget, and event-driven communication patterns and their tradeoffs
Request-response, fire-and-forget, and event-driven side by side, with the tradeoffs each pattern makes.

This inversion is the point. Adding a new reaction to OrderPlaced, say a loyalty-points service, requires zero changes to the order service. You subscribe a new consumer and you are done. That loose coupling makes event-driven microservices architecture attractive as systems grow and teams multiply.

The tradeoffs are real. Event-driven systems trade the simplicity of a direct call for eventual consistency, harder debugging, and the need to reason about ordering and idempotency. When five consumers react to one event, "what is the current state" no longer has a single synchronous answer. This is the same coupling-versus-flexibility tension teams weigh in a modular monolith versus microservices debate.

Fire-and-forget vs event-driven: the distinction that trips people up

These two get conflated because both are asynchronous and both often ride the same broker. The difference is direction of knowledge.

Fire-and-forget is command-shaped. The producer says "do this thing" and names the work: "send this email." There is usually one logical handler. The producer is delegating.

Event-driven is fact-shaped. The producer says "this happened" and names nothing about who responds: "order was placed." There can be zero, one, or ten consumers. The producer is broadcasting.

Get this backward and you build brittle systems. If your "event" secretly assumes one consumer doing one thing, you have coupled a command to an event's clothing, and the day someone adds a second consumer, ordering and duplicate bugs appear.

A decision framework you can use

Skip the dogma. Ask three questions in order.

First, does the caller need the answer to continue right now? If yes, use request-response. Rendering a page, authorizing a payment, and validating input all qualify. Do not make these asynchronous just because async is fashionable.

Second, if the caller does not need the answer, does anyone need to know the work happened? If it is a single downstream task the producer is delegating, fire-and-forget with a durable queue is the simplest fit. Emails, notifications, and background jobs live here.

Third, might multiple independent services care about this occurrence, now or later? If yes, model it as an event and publish a fact. This buys extensibility at the cost of eventual consistency, so spend it where the flexibility pays off.

Comparing the three patterns

The patterns differ across the dimensions that bite you in production. Request-response pays the full round trip, while both asynchronous patterns return immediately. Request-response is tightly coupled in time; fire-and-forget is loosely coupled to one consumer; event-driven is loosely coupled to many. A synchronous dependency failure fails the caller, while asynchronous failures are absorbed by the queue and retried. Request-response gives read-your-writes immediacy, while asynchronous patterns give eventual consistency and require distributed tracing to debug.

None is strictly better. Mature systems use all three. A single checkout flow might read the cart synchronously, fire-and-forget the receipt email, and publish an OrderPlaced event for inventory, analytics, and fraud detection to consume.

Common mistakes to avoid

The first mistake is synchronous chaining: A calls B calls C calls D, all blocking, so your p99 latency and availability are hostage to the slowest link. Break these chains with asynchronous handoffs wherever the caller does not truly need the downstream result.

The second is asynchronous everything. Teams discover queues and route even correctness-critical reads through them, then fight eventual-consistency bugs for months. Asynchronous communication is a tool for decoupling, not a religion.

The third is skipping durability. Fire-and-forget and event-driven only work if the transport is durable and consumers are idempotent with retries and dead-letter handling.

The fourth is ignoring backpressure. Producers can outrun consumers. Without bounded queues, rate limits, or load shedding, your broker becomes the bottleneck under exactly the traffic spikes you built the system to survive.

Bringing it together

Synchronous versus asynchronous communication is not a style preference. It is a load-bearing decision that determines your latency, failure modes, and coupling. Request-response gives immediacy at the cost of temporal coupling. Fire-and-forget gives responsiveness for delegated work at the cost of certainty. Event-driven gives extensibility at the cost of eventual consistency.

The engineers who design resilient systems ask, for each interaction, whether the caller needs the answer now, whether anyone needs to know the work happened, and whether multiple services might care. Answer those honestly and the right pattern picks itself. For more breakdowns, explore the Levelop blog, or start at Levelop.

Frequently asked questions

What is the difference between synchronous and asynchronous communication?

Synchronous communication means the caller blocks and waits for a response before continuing, so the two services are coupled in time. Asynchronous communication means the caller hands off the work and continues immediately, with any reply arriving later through a queue, callback, or event. The practical consequence is that a slow or failed dependency stalls a synchronous caller but not an asynchronous one.

When should I use request-response instead of an event?

Use request-response when the caller genuinely cannot proceed without the answer, such as authorizing a payment, validating a token, or reading data to render a page. If the caller does not need the result to continue, prefer an asynchronous pattern so a slow dependency does not inflate your latency or availability risk.

Is fire-and-forget the same as event-driven architecture?

No. Fire-and-forget is command-shaped: the producer delegates a specific task, usually to one handler, and does not wait for the result. Event-driven is fact-shaped: the producer announces that something happened without knowing who, if anyone, will react. Both are asynchronous and often share a broker, but their coupling and intent differ.

How do I prevent data loss with asynchronous communication?

Use a durable message broker so messages survive restarts, make consumers idempotent so at-least-once delivery does not cause duplicate side effects, add automatic retries with backoff, and route persistent failures to a dead-letter queue for inspection. Without durability, asynchronous handoffs silently lose work whenever a process crashes.

Does asynchronous communication always improve performance?

Not always. It improves perceived responsiveness by removing waits from the caller's critical path, but it adds eventual consistency, harder debugging, and operational overhead like backpressure and ordering concerns. For correctness-critical reads, a simple synchronous call is often faster to build and entirely sufficient.

Keep reading

System Design

API Gateway Best Practices: Taming One Failure Domain

An API gateway merges auth, throttling, routing and transformation into one process, making it a correlated failure domain for every service behind it. The failure modes, and what contains them.

Read article
System Design

Bloom Filters Explained: When "Probably Yes" Is Good Enough and Saves 99% of Memory

Bloom filters trade a tiny false positive rate for 95% memory savings. Here is how they work and where they show up in Cassandra, Redis, and Chrome.

Read article
System Design

Consistent Hashing Explained: Virtual Nodes and Why They Matter for Distributed Systems

Learn how consistent hashing with virtual nodes solves load balancing and data distribution in distributed systems like Cassandra, DynamoDB, and Redis Cluster.

Read article