All notes

Synchronous versus asynchronous isn't really about speed

Synchronous and asynchronous describe how work is coordinated, not whether the work itself is fast or slow.

about 26 minutes min read

I had been using these words as though they described performance:

synchronous  = slow
asynchronous = fast

That interpretation is attractive.

A synchronous request often involves somebody waiting. An asynchronous design often lets the sender continue sooner.

But the work itself has not necessarily become faster.

I can place a thirty-minute job onto a queue in ten milliseconds. The submission was quick. The job still takes thirty minutes.

Likewise, a synchronous operation that returns a cached value in two milliseconds is still synchronous.

The more useful distinction is:

Synchronous and asynchronous describe how participants coordinate around completion, not how quickly the underlying work executes.

In this Note, I am mainly using those words to describe the relationship between a caller and the completion of a distributed operation. Programming-language and operating-system APIs also use them, sometimes with narrower meanings.

That changes the questions I need to ask.

Instead of:

Which option is faster?

I should ask:

Who remains dependent on the result?

What outcome must be known before they continue?

Can the unfinished work receive a separate lifecycle?

What guarantee marks a successful handoff?

How will eventual completion or failure be observed?

Coordination around completion

Suppose a customer requests product information.

customer
   │
   ▼
storefront
   │
   ▼
catalog service

The storefront cannot render the requested product until it knows:

name
description
price
availability

A direct interaction fits naturally:

STOREFRONT
    │
    │ GetProduct
    ▼
CATALOG SERVICE
    │
    │ Product
    ▼
STOREFRONT CONTINUES

The storefront’s current operation remains logically dependent on the result. Its implementation might block a thread, suspend a coroutine or register a future, but the workflow cannot continue meaningfully until the direct outcome is known.

That is a synchronous relationship.

It might take:

2 milliseconds
200 milliseconds
20 seconds

The duration affects whether it is acceptable. It does not change the coordination model.

A synchronous outcome might be:

success
rejection
not found
timeout
service unavailable

Synchronous does not promise success. It means the caller expects the direct interaction to produce the outcome needed for its next decision.

Now suppose an order has been created and the system needs to send a confirmation email.

The order service does not necessarily need to wait while another system:

selects a template
renders the message
contacts an email provider
waits for provider acceptance

It could hand off a command:

SendOrderConfirmation

or publish a fact:

OrderCreated

The workflow becomes:

ORDER SERVICE
      │
      │ hand off work
      ▼
DURABLE WORKFLOW SYSTEM
      │
      │ accepts responsibility
      ▼
ORDER SERVICE CONTINUES


later:


NOTIFICATION CONSUMER
      │
      ▼
send email

The final business outcome:

customer confirmation sent

has its own lifecycle outside the original handoff interaction.

That is an asynchronous relationship.

The essential distinction is therefore:

SYNCHRONOUS

the caller's current contract depends on
the direct outcome


ASYNCHRONOUS

final completion continues through
a separate observable lifecycle

Technology does not decide the coordination model

It is tempting to map technologies directly onto these words:

HTTP       = synchronous
messaging  = asynchronous

That shortcut is unreliable.

HTTP provides a direct request-response contract: each request has a corresponding response. The client API may be blocking or non-blocking, and the larger business operation may complete during that response or continue afterward.

For example, a report API can accept work for later processing:

POST /reports HTTP/1.1
Content-Type: application/json

{
  "month": "2021-06"
}

Response:

HTTP/1.1 202 Accepted
Location: /report-jobs/8472
Content-Type: application/json

{
  "job_id": "8472",
  "status": "pending"
}

The initial HTTP exchange completed. The report did not.

The client can inspect the operation later:

GET /report-jobs/8472 HTTP/1.1
Host: reports.example

Response while processing:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "job_id": "8472",
  "status": "running"
}

Later:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "job_id": "8472",
  "status": "completed",
  "report_url": "/reports/2021-06"
}

The HTTP interaction was request-response. The business workflow was asynchronous.

Messaging can also support a synchronous relationship.

A caller can send a request message containing:

request ID
reply destination

and wait for a correlated reply:

CALLER
   │
   │ request message
   ▼
BROKER
   │
   ▼
RESPONDER
   │
   │ reply message
   ▼
BROKER
   │
   ▼
CALLER WAITS FOR MATCHING REPLY

The transport uses messaging, but the caller still requires the direct result before continuing.

That can combine the operational complexity of a broker with the temporal coupling of request-response, which is quite an architectural tasting menu.

The question remains:

Can the caller honour its current contract without learning the final result now?

Blocking, concurrency and parallelism are different dimensions

I had also been blending:

synchronous
blocking

A blocking call prevents the executing thread from progressing until something happens.

thread calls receive
       │
       ▼
no data available
       │
       ▼
thread waits
       │
       ▼
data arrives
       │
       ▼
call returns

A non-blocking or asynchronous programming API may expose completion through a:

callback
future
promise
event
completion queue
channel

But a web server can use non-blocking I/O while its business operation remains logically dependent on a downstream response.

BLOCKING / NON-BLOCKING
    │
    └── what happens to the executing thread?


SYNCHRONOUS / ASYNCHRONOUS
    │
    └── how is the workflow coordinated
        with completion?

Concurrency and parallelism are separate again.

CONCURRENCY

work overlaps in time


PARALLELISM

work executes simultaneously


ASYNCHRONY

completion follows a separate
coordination path

Suppose checkout calls inventory and shipping concurrently:

                   CHECKOUT
                      │
          ┌───────────┴───────────┐
          │                       │
          ▼                       ▼
     INVENTORY                 SHIPPING
          │                       │
          └───────────┬───────────┘
                      ▼
             CHECKOUT CONTINUES

The calls may save time by overlapping, but checkout still requires both results before continuing.

The workflow remains synchronous at that boundary.

A synchronous system can perform work concurrently. An asynchronous system can process one job at a time. Speed, overlap and completion ownership are different dimensions.

Acknowledgement is not completion

Suppose a producer publishes:

GenerateInvoice

The messaging system replies successfully.

What has succeeded?

Possibilities include:

the message entered local process memory
the broker received it
the broker stored it durably
a replica stored it
a consumer received it
a consumer acknowledged delivery
the invoice was generated
the invoice was stored successfully

Those are different milestones.

A useful asynchronous handoff exists only when the contract defines where responsibility changes hands.

PRODUCER
    │
    │ publish
    ▼
BROKER
    │
    │ acknowledgement
    ▼
PRODUCER CONTINUES

At this point the producer might know:

The messaging system accepted responsibility under the configured durability guarantees.

It does not automatically know:

The downstream business outcome completed.

“Queued” is useful only when the contract says what was stored, where it was stored and which failures can still lose it.

There are also several acknowledgements in a brokered workflow:

producer → broker
    publication acknowledgement

broker → consumer
    delivery

consumer → broker
    delivery acknowledgement

consumer → business system
    completed external effect

A consumer acknowledgement may tell the broker that the message reached an agreed processing point. It does not prove that every later business consequence succeeded.

The design needs precise language for:

accepted
durably recorded
queued
processing
completed
failed

Calling all of those states “successful” creates a semantic soup.

Time and uncertainty

A synchronous caller cannot wait forever. It needs a deadline.

caller sends request
        │
        ▼
waits for result
        │
        ├── result before deadline
        │       └── continue with result
        │
        └── deadline expires
                └── stop waiting

The deadline answers:

How long is this result still useful to the caller?

But a timeout creates uncertainty.

The caller knows:

I did not receive a result in time

It may not know:

the server did nothing
the server is still working
the server completed successfully
the response was lost

A timeout is the caller’s conclusion about waiting. It is not proof about the receiver’s final state.

That is one of the central differences between a local call and a remote interaction.

Cancellation has the same limit.

caller cancels
      │
      ├── receiver has not started
      │       └── may avoid the work
      │
      ├── receiver is working
      │       └── may stop cooperatively
      │
      └── receiver already committed
              └── effect remains

Cancellation often means:

I no longer intend to wait for this result.

It does not automatically mean:

Reverse every effect the receiver has already produced.

The receiver may notice the cancellation, ignore it, finish before receiving it or have crossed an irreversible boundary already.

Retries therefore need a repeated-execution contract.

A timeout followed by a retry can produce:

attempt 1 completes remotely
response is lost

attempt 2 arrives

For state-changing operations such as:

create order
charge payment
reserve stock
send email

the receiver may need a stable operation identifier:

operation_id = checkout-7f82...

with a policy such as:

same identifier + same request
      │
      └── return the original outcome

same identifier + different request
      │
      └── reject as conflict

Retries also need limits.

A reasonable policy may consider:

which failures are retryable
overall deadline
maximum attempts
backoff
jitter
whether the work remains useful

If many clients retry an overloaded service immediately, resilience can become a retry storm.

service overloaded
      │
      ▼
clients retry together
      │
      ▼
service becomes more overloaded

Idempotency prevents repeated business effects. It does not make unlimited retries operationally harmless.

Acceptance, queueing and completion are separate measurements

Asynchronous submission can return quickly while the work remains slow.

Imagine generating a large monthly report.

A synchronous API could keep the client attached while the service:

queries data
builds report
stores report
returns result

An asynchronous API can return after:

validating request
recording job
returning identifier

The architecture changed how waiting is managed. It did not accelerate report generation by vocabulary.

A more useful latency model is:

SUBMISSION LATENCY

time until responsibility is accepted


QUEUE DELAY

time waiting before processing begins


SERVICE TIME

time spent performing the work


COMPLETION LATENCY

total time from request to business outcome

For synchronous work, queue delay may be hidden inside the server before the response.

For asynchronous work, it often appears as message age or job wait time.

A service can have excellent submission latency and terrible completion latency. Monitoring only acceptance would tell a very cheerful but incomplete story.

Asynchrony can still enable genuine performance improvements through:

batching
parallel consumers
load smoothing
coalescing duplicate work
better resource utilisation

Those improvements come from the execution design. Asynchrony itself does not automatically reduce the service time of one task.

Asynchronous work needs time limits too

Removing a waiting caller does not mean work can remain pending forever.

An asynchronous job may need:

maximum queue age
processing deadline
expiry time
retry limit
business cut-off time

For example:

Send the dispatch notification within ten minutes.

A message processed three days later may be technically completed and operationally useless.

The workflow needs to answer:

How long may work wait?

When is the result no longer valuable?

Should stale work be discarded?

Should expiry be reported as failure?

Can an old command still be applied safely?

A queue preserves work. It does not preserve its relevance indefinitely.

A queue is a waiting room, not an accelerator

Suppose work arrives faster than consumers can process it.

Without buffering:

incoming rate > processing rate
          │
          ▼
requests fail or callers wait

With a queue:

incoming rate > processing rate
          │
          ▼
backlog grows
          │
          ▼
consumers process later

If work arrives at:

100 jobs per second

and consumers complete:

60 jobs per second

then the backlog grows by roughly:

40 jobs per second

until:

arrival rate drops
consumer capacity increases
messages expire
storage fills

The queue can absorb a temporary burst. It does not increase the consumers’ underlying capacity.

It converts immediate pressure into stored waiting while its storage, retention and recovery limits remain healthy.

Useful infrastructure signals include:

queue depth
oldest message age
queue delay
consumer throughput
consumer failures
retry volume
dead-letter volume
storage capacity

But the strongest measure is often the business promise:

percentage of exports completed within ten minutes

A quiet API and a rapidly ageing queue are not evidence of a healthy system. They are evidence that the waiting moved somewhere less visible.

Backpressure still reaches somebody

A direct synchronous interface creates immediate pressure because callers wait or fail when capacity is exhausted.

A brokered interface can postpone that pressure, but not abolish it.

Eventually the system may need to:

slow producers
reject new work
scale consumers
shed low-priority work
expire stale messages
increase storage

That is backpressure at the system level.

Asynchronous design can isolate a producer from a short consumer outage by converting immediate failure into delayed completion and backlog growth.

That isolation lasts only while:

the broker remains available
publication still succeeds
retention is sufficient
storage remains healthy
delay stays acceptable

If the consumer remains unavailable, the business outcome remains incomplete.

The failure has been deferred and made observable in another place. It has not been cured.

Queues are elastic-looking until the mathematics arrives wearing steel-toed boots.

Asynchronous does not mean fire and forget

The phrase:

fire and forget

makes asynchronous work sound delightfully responsibility-free.

But somebody still needs to know:

Was the handoff accepted?

Was it stored durably?

Was it eventually consumed?

Did processing succeed?

Will failures be retried?

When should retries stop?

Where do permanently failed messages go?

Who owns recovery?

The system has not forgotten. It has distributed the responsibility across more components.

A more honest description is:

hand off and observe

The sender may no longer wait for completion. The organisation still cares whether completion happens.

In a synchronous interaction, failure often returns through the direct response.

request
   │
   ▼
receiver fails
   │
   ▼
error response
   │
   ▼
caller handles error

In an asynchronous workflow, publication can succeed while processing fails later.

publish message
      │
      ▼
publication acknowledged
      │
      ▼
sender continues
      │
      ▼
consumer fails later

The architecture needs another route for failure information:

retry queue
delayed retry
failed-job record
operator alert
compensating workflow
user-visible status
dead-letter queue

A dead-letter queue can prevent an endless retry loop. It does not resolve the failed business operation. It still needs ownership, alerting and a recovery policy.

Operation resources make unfinished work visible

Suppose a user requests a large data export.

The interface returns:

export accepted

The user still needs a way to discover:

whether it started
whether it completed
whether it failed
where the result can be found

A job resource makes the lifecycle explicit:

EXPORT JOB 8472
│
├── requested
├── queued
├── running
├── completed
├── failed
└── expired

The resource can have:

identifier
status
creation time
start time
completion time
failure reason
result location
expiry policy

This turns hidden background work into a first-class system object.

It is often more useful than returning 202 Accepted and hoping everybody achieves spiritual closure.

Repeated delivery still needs idempotency

Suppose a consumer receives a message, performs the business effect and fails before acknowledging delivery.

consumer receives message
          │
          ▼
updates database
          │
          ▼
fails before acknowledgement
          │
          ▼
message delivered again

The next processing attempt may repeat the effect.

Examples include:

send email twice
reserve stock twice
create two invoices
apply credit twice

The consumer still needs a stable identity for the logical work and a way to recognise completion.

Different delivery models make different trade-offs:

AT-MOST-ONCE

loss is possible
duplicate delivery is avoided


AT-LEAST-ONCE

redelivery is possible
consumers must handle duplicates


EXACTLY-ONCE

meaningful only within a clearly
defined processing boundary

A broker may offer exactly-once behaviour inside a particular log or transaction model. That does not automatically make an arbitrary external effect, such as sending an email or charging a card, happen exactly once.

Repeated attempt is not automatically safe repeated effect.

Cron taught the same lesson. Asynchronous systems simply provide more creative ways for the same logical work to arrive twice.

Ordering must be defined at the right boundary

Synchronous code often creates an apparent sequence:

perform A
wait
perform B
wait
perform C

Asynchronous work may overlap:

publish A
publish B
publish C

Several different orderings can then matter:

delivery order
processing-start order
processing-completion order
effect-commit order

A broker can preserve delivery order while concurrent consumers still complete work out of order.

For state transitions such as:

OrderCreated
OrderCancelled

the system may need an ordering boundary:

per order
per customer
per partition
per queue

A universal total order is often unnecessary and expensive.

The useful question is:

Which related operations require ordering, and at which stage must that order hold?

Asynchronous does not mean unordered. It means required order must be designed rather than accidentally inherited from a call stack.

Consistency is a separate contract

Asynchronous propagation often makes inconsistency visible.

product price changed
      │
      ▼
publish event
      │
      ▼
search index updates later

For a period:

catalog database:
    new price

search index:
    old price

The system has an explicit consistency interval.

The design needs to answer:

How stale may the secondary view be?

How will lag be measured?

What happens when propagation fails?

Can the view be rebuilt?

But synchrony does not automatically guarantee global consistency.

A service may return success while:

replicas still lag
caches still contain old values
search indexes update later
other services have not observed the change

A synchronous response can tell the caller that the direct operation reached an agreed outcome. It does not prove that every system view is already consistent.

Consistency and coordination are related design concerns, but neither word determines the other.

State changes and messages create a publication boundary

Suppose an order service needs to:

1. commit the order
2. publish OrderCreated

Those are two separate writes.

The service can fail between them:

database commits
      │
      ▼
process fails before publishing
      │
      ▼
order exists, event missing

Publishing first creates the opposite risk:

event published
      │
      ▼
database transaction fails
      │
      ▼
event describes a fact
that never committed

Moving work out of the request path creates a new reliability boundary.

One common design is a transactional outbox:

single database transaction
      │
      ├── store business change
      └── store outgoing message record
                   │
                   ▼
          separate publisher retries
          delivery from the outbox

The business state and the intention to publish are recorded together locally. A separate process can then retry publication without inventing a new business event each time.

The pattern does not remove duplicate delivery or every failure mode. It closes the gap between committing local state and remembering that a message must be sent.

User experience decides where waiting belongs

Sometimes the customer needs an immediate answer.

For example:

Was my payment authorised?

Displaying:

We'll process that eventually

may be unacceptable.

Other work can happen later:

send receipt
update recommendations
feed analytics
notify warehouse

One user action can therefore contain both coordination styles.

CUSTOMER CLICKS PLACE ORDER
             │
             ▼
      validate checkout
             │
             ▼
      authorise payment
             │
             ▼
        create order
             │
             ▼
   return confirmed order
             │
             ├──► publish notification work
             ├──► publish analytics event
             └──► publish fulfilment event

The synchronous boundary should include what must be true before the system can honestly confirm the order.

Everything after that boundary needs a durable lifecycle and observability of its own.

The goal is not to make the response as quick as possible by placing all responsibility on a queue.

The goal is to return at the earliest point where the response remains truthful.

Responsiveness and completion are different results

Suppose confirmation email takes three seconds.

If checkout waits:

create order             200 ms
send email             3,000 ms
return response           100 ms
                       --------
client waits            3,300 ms

If the email is handed off:

create order             200 ms
publish email command     20 ms
return response           100 ms
                       --------
client waits              320 ms

The user receives a response sooner.

Email delivery still takes approximately three seconds after processing begins.

The design improved:

checkout response latency

It did not necessarily improve:

email service time

That is a more precise performance claim.

Synchronous paths propagate dependencies

Consider a request chain:

CLIENT
  │
  ▼
SERVICE A
  │
  ▼
SERVICE B
  │
  ▼
SERVICE C

If A needs B’s result and B needs C’s result, then C’s slowness can affect the entire path.

C slows
  │
  ▼
B waits
  │
  ▼
A waits
  │
  ▼
client waits

Every direct dependency added to the critical path extends the collection of things that must respond within the caller’s deadline.

That does not mean every call should become a message. It means the synchronous path should contain only dependencies required to honour the current contract.

Choosing the completion boundary

A synchronous relationship may fit when:

the caller needs the answer now

the result determines the next immediate action

the operation normally completes within the caller's deadline

the direct dependency is acceptable

the outcome should be returned through this interaction

An asynchronous relationship may fit when:

the caller does not need final completion now

work may be delayed safely

a short consumer outage should become backlog rather than immediate failure

traffic arrives in bursts

several consumers need the same fact

the work benefits from an independent lifecycle

The choice is not:

slow technology
versus
fast technology

It is:

keep completion in this interaction
versus
give completion its own lifecycle

A practical comparison

Question Synchronous interaction Asynchronous interaction
Does the caller require the direct outcome? Usually No, after a defined handoff
Must the result-producing path be available? Yes Not necessarily at handoff time
Is final failure returned directly? Often Usually through another mechanism
Can work wait in a backlog? Not naturally Often
Is final completion immediately known? Usually Usually not
Does the design need deadlines? Yes Yes, including age and processing limits
Does it need retry handling? Yes Yes
Does it need idempotency? Often Very often
Can it be fast? Yes Yes
Can it be slow? Yes Spectacularly

The table has no universal winner. It shows which responsibilities move when the completion boundary changes.

One operation can have several synchronisation points

A workflow is not always entirely synchronous or entirely asynchronous.

Consider a product-image upload:

1. client uploads image
2. service stores original
3. service records asset
4. service returns asset ID
5. thumbnail generation occurs later
6. metadata extraction occurs later
7. search index updates later

The initial interaction might synchronously guarantee:

original safely stored
asset ID created

Then asynchronous processing handles:

thumbnail generation
metadata extraction
search indexing

The important question is:

What must be complete before the initial response can honestly claim success?

Everything before that boundary belongs to the direct contract.

Everything after it needs:

durable handoff
identity
status
deadlines
retries
idempotency
failure ownership
observability

The mental model I’m keeping

My old model was:

SYNCHRONOUS
    │
    └── slow


ASYNCHRONOUS
    │
    └── fast

The new model is:

SYNCHRONOUS
    │
    └── caller remains coordinated
        with the direct outcome


ASYNCHRONOUS
    │
    └── final completion receives
        a separate lifecycle

Speed is another dimension:

                   FAST              SLOW

SYNCHRONOUS     cached lookup     long request

ASYNCHRONOUS    tiny queued job   overnight report

Other dimensions remain independent:

blocking versus non-blocking

sequential versus concurrent

direct versus brokered

accepted versus completed

immediate versus delayed visibility

submission latency versus completion latency

So when somebody says:

We should make this asynchronous because it is slow.

I need to ask:

Does the caller actually need the result?

Can the work safely outlive the request?

What guarantee marks a successful handoff?

How will completion be observed?

Where will later failure appear?

How much delay is acceptable?

What happens when the backlog grows?

Can repeated delivery repeat the business effect?

How will state change and message publication agree?

Perhaps asynchronous processing is the right answer.

But the reason is not that asynchronous work runs through a secret faster section of the computer.

It is that the system can place waiting, responsibility and failure somewhere more appropriate.

The work still takes however long the work takes.

Architecture decides who has to stand beside it while it does.

References and further reading

HTTP asynchronous processing

RFC 7231: HTTP/1.1 Semantics and Content Defines HTTP methods and status codes used in 2021, including 202 Accepted, which indicates that a request has been accepted for processing but has not yet completed.

RFC 7240: Prefer Header for HTTP Defines the Prefer request header, including the respond-async preference and an example of asynchronous HTTP processing using 202 Accepted.

RPC deadlines and cancellation

gRPC: Core concepts, architecture and lifecycle Explains RPC lifecycles, deadlines, cancellation and the distinction between the client and server’s views of an RPC’s completion.

gRPC and Deadlines Explains why clients should place time bounds on RPC calls and how deadline expiry affects the caller’s wait for a result.

Brokered messaging

RabbitMQ: AMQP 0-9-1 Model Explained Introduces exchanges, queues, bindings, consumers and acknowledgements in RabbitMQ’s messaging model.

RabbitMQ Tutorial: Work Queues Demonstrates asynchronous task distribution, competing consumers, acknowledgements and redelivery after consumer failure.

Event streaming and consumer progress

Apache Kafka 2.8 Documentation The Kafka documentation for the version current when this Note was written, covering producers, consumers, topics, partitions, retention and consumer groups.

Apache Kafka 2.8: Distribution Explains consumer offset tracking and group coordination, which allow processing progress to be recorded independently of the producer’s original publication.

Asynchronous programming interfaces

POSIX.1-2017: Asynchronous Input and Output Documents the POSIX asynchronous I/O interface, providing a useful contrast between asynchronous program execution mechanisms and asynchronous communication between distributed services.