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.