When Asynchronous Processing Made the System Less Reliable

We had a performance problem.

One part of the application was doing too much work during a synchronous request.

The obvious solution was to move the work out of the request path.

Instead of:

Client
  │
  ▼
API
  │
  ▼
Process Everything
  │
  ▼
Response

we changed the flow to:

Client
  │
  ▼
API
  │
  ▼
Queue
  │
  ▼
Worker
  │
  ▼
Process

The API became faster.

The system could absorb bursts more easily.

Workers could scale independently.

On paper, the architecture was better.

Then we discovered something uncomfortable.

The system had become harder to make reliable.

The problem wasn’t the queue.

The problem was that asynchronous processing had changed the meaning of success.

1. The Synchronous Problem

The original workflow was straightforward.

A request arrived.

The application performed the required work.

The database was updated.

The application returned a response.

Conceptually:

Request
   │
   ▼
Validate
   │
   ▼
Process
   │
   ▼
Update State
   │
   ▼
Response

The advantage was simplicity.

When the API returned success, the operation had completed.

When something failed, the caller generally knew immediately.

But the approach had a limitation.

Some operations were expensive.

During periods of increased traffic, requests spent too much time waiting for downstream processing.

Threads remained occupied.

Queues formed inside the application.

Latency increased.

So we had a reasonable architectural question:

Did this work really need to happen before the API responded?

For some operations, the answer was no.

That led us toward asynchronous processing.

2. The Asynchronous Design

The new design separated accepting the request from performing the work.

The API would validate the request, record the necessary state, publish a message, and return.

The worker would process the message later.

Something like:

                ┌─────────────┐
                │    Client   │
                └──────┬──────┘
                       │
                       ▼
                ┌─────────────┐
                │     API     │
                └──────┬──────┘
                       │
                       ▼
                ┌─────────────┐
                │    Queue    │
                └──────┬──────┘
                       │
              ┌────────┴────────┐
              ▼                 ▼
        ┌───────────┐     ┌───────────┐
        │  Worker 1 │     │  Worker 2 │
        └─────┬─────┘     └─────┬─────┘
              │                 │
              └────────┬────────┘
                       ▼
                  ┌─────────┐
                  │Database │
                  └─────────┘

This gave us several useful properties.

The API no longer had to wait for the entire operation.

The queue could absorb temporary traffic spikes.

Workers could scale independently.

Slow downstream processing didn’t necessarily block incoming requests.

The architecture had become more elastic.

But it had also introduced a new question:

What exactly does it mean when the API says the request succeeded?

3. The Meaning of Success Had Changed

This was the first important realization.

In the synchronous model:

API success
     =
Operation completed

In the asynchronous model:

API success
     =
Request accepted

Those are not the same thing.

The API could successfully accept a request while the actual business operation was still waiting in a queue.

The worker could fail later.

The database operation could fail later.

The message could be retried later.

The consumer could crash after performing the operation but before acknowledging the message.

The system had moved part of the failure window outside the original request.

That wasn’t necessarily bad.

But it meant we had to design for it explicitly.

4. The Failure We Didn’t Expect

The most interesting failure involved message delivery.

Consider a worker processing a message:

1. Receive message
2. Process business operation
3. Commit database transaction
4. Acknowledge message

Now imagine something happens between steps 3 and 4.

The database transaction succeeds.

The business state has changed.

But the worker crashes before the message acknowledgment reaches the broker.

From the message broker’s perspective:

The message was never successfully processed.

So the broker delivers it again.

The worker receives the same message.

It processes it again.

Now we have:

Message
   │
   ▼
Worker
   │
   ▼
Database update ✓
   │
   X
   │
Acknowledgment lost
   │
   ▼
Message redelivered
   │
   ▼
Worker processes again

The system has a perfectly valid retry.

But the business operation has happened twice.

This is where asynchronous systems become fundamentally different from simple request-response systems.

5. The Problem Wasn’t the Retry

At first glance, the natural reaction is:

“Why did the message get delivered twice?”

But duplicate delivery is not necessarily a messaging failure.

In many distributed systems, consumers have to assume that a message may be delivered more than once.

The deeper problem is:

Could our business operation safely be executed twice?

If the answer is no, the consumer needs idempotency.

For example, imagine a worker receiving:

Process Payment
paymentId = 847291
amount = $500

The worker shouldn’t blindly assume that every delivery represents a new payment.

It needs to determine whether:

paymentId = 847291

has already been processed.

Conceptually:

Receive message
      │
      ▼
Check idempotency state
      │
   ┌──┴───┐
   │      │
 New    Already processed
   │      │
   ▼      ▼
Process   Ignore / return safely
   │
   ▼
Record completion

The important point is that message delivery and business execution are different concerns.

6. We Had Created a New Consistency Problem

The synchronous design had hidden much of this complexity because the operation happened within one request flow.

The asynchronous design separated the workflow across time and processes.

Now we had multiple states to reason about:

Request received
       ↓
Message published
       ↓
Message consumed
       ↓
Business operation started
       ↓
Business operation completed
       ↓
Message acknowledged

Those steps don’t necessarily happen atomically.

A failure can occur between almost any two of them.

That creates states such as:

Request accepted
Message published
Business operation not completed

or:

Business operation completed
Message not acknowledged

or:

Message consumed
Worker crashed
Operation status uncertain

The system can still be correct.

But correctness now depends on how these states are modeled and reconciled.

7. The Tempting Fix: Just Make Everything Synchronous Again

At this point, someone could reasonably ask:

Why not just go back to synchronous processing?

Sometimes that is exactly the right answer.

Asynchronous processing isn’t automatically better.

It introduces:

  • Queues
  • Consumers
  • Retry policies
  • Duplicate delivery
  • Ordering concerns
  • Dead-letter handling
  • Eventual consistency
  • Additional observability
  • More operational components

If the work is small and must complete before the caller can continue, synchronous processing may be simpler and more reliable.

But in our case, asynchronous processing solved a real problem.

The API no longer needed to wait for expensive downstream work.

The queue provided buffering during traffic spikes.

Workers could scale independently.

So the answer wasn’t to remove asynchronous processing.

It was to make its failure semantics explicit.

8. The First Change: Idempotency

The most important change was making the consumer idempotent.

Every business operation needed a stable identifier.

For example:

operationId = 847291

The consumer could then determine whether that operation had already been successfully applied.

A simplified model:

┌──────────────────────────┐
│ Idempotency Record       │
├──────────────────────────┤
│ operation_id             │
│ status                   │
│ processed_at             │
└──────────────────────────┘

The exact implementation depends on the business operation and storage model.

The principle is more important:

Repeated delivery must not accidentally create repeated business effects.

This is especially important when the operation involves money, inventory, account state, notifications, or any other non-reversible side effect.

9. The Second Change: Make State Explicit

We also had to stop treating the operation as simply:

SUCCESS / FAILURE

Asynchronous workflows often need more states.

For example:

RECEIVED
   ↓
QUEUED
   ↓
PROCESSING
   ↓
COMPLETED

with possible paths to:

FAILED
RETRYING
DEAD_LETTERED

This makes the lifecycle visible.

It also makes recovery easier.

Instead of asking:

“Did the operation happen?”

we can ask:

“What state is the operation currently in?”

That is a much more useful question in a distributed system.

10. The Third Change: Control Retries

Retries are useful.

But retries without limits can turn a temporary failure into a sustained failure.

Suppose a downstream dependency is unavailable.

The worker fails.

The message is retried.

It fails again.

The message is retried immediately.

Again.

And again.

Now imagine hundreds or thousands of messages doing the same thing.

The retry mechanism itself becomes additional load on the failing dependency.

This creates a feedback loop:

Dependency slows down
        ↓
Requests fail
        ↓
Messages retry
        ↓
More requests arrive
        ↓
Dependency receives more load
        ↓
Dependency slows further

Retries need boundaries.

Depending on the system, that can include:

  • Maximum retry attempts
  • Exponential backoff
  • Jitter
  • Dead-letter queues
  • Circuit breakers
  • Rate limiting
  • Explicit recovery workflows

The objective isn’t to retry forever.

It is to give a transient failure a reasonable opportunity to recover without turning the failure into a larger outage.

11. The Fourth Change: Observability

Asynchronous systems also change how you debug production problems.

With synchronous processing, you can often follow:

Request → Service → Database → Response

With asynchronous processing, the request might return before the business operation is completed.

The flow becomes:

Request
   │
   ▼
API
   │
   ▼
Message
   │
   ├─────────────── time passes ───────────────┐
   │                                           │
   ▼                                           ▼
Queue                                      Worker
                                               │
                                               ▼
                                           Database

A log line in the API is no longer enough.

We need to connect the entire workflow.

That means carrying identifiers such as:

requestId
operationId
messageId
correlationId

across the asynchronous boundary.

Then we can answer:

  • When was the request received?
  • When was the message published?
  • When was it consumed?
  • How many times was it retried?
  • Which worker processed it?
  • Did the database operation succeed?
  • Was the message acknowledged?
  • Did it eventually reach a dead-letter queue?

Without this information, asynchronous failures can become extremely difficult to reconstruct.

12. Ordering Became Another Architectural Question

There was another issue we couldn’t ignore.

Suppose two messages represent changes to the same business entity:

Message A: Increase balance
Message B: Decrease balance

If the system requires strict ordering, processing B before A may produce an incorrect result.

Asynchronous processing introduces questions that synchronous execution may have hidden:

Does ordering matter?

If it does:

Where is ordering guaranteed?

And:

What happens if messages are delayed or retried?

There isn’t one universal answer.

Some systems can process events independently.

Others require ordering per account, customer, transaction, or aggregate.

The important part is not choosing a particular messaging technology.

It is understanding the business semantics before designing the messaging model.

13. Eventual Consistency Wasn’t a Bug

Another misconception we encountered was expecting every part of the system to reflect the change immediately.

In an asynchronous architecture, that may not be possible or even necessary.

For example:

Request accepted
      ↓
Payment event published
      ↓
Payment processing
      ↓
Ledger update
      ↓
Notification

Different components may observe those state changes at slightly different times.

That is eventual consistency.

The problem isn’t eventual consistency itself.

The problem is introducing it without understanding where it is acceptable.

For some workflows:

A few seconds of delay

may be perfectly acceptable.

For others:

The balance must be authoritative before proceeding

may require synchronous confirmation.

The architecture has to follow the business invariant.

Not the other way around.

14. What We Learned About Asynchronous Architecture

The biggest lesson wasn’t:

“Asynchronous processing is dangerous.”

It wasn’t.

Asynchronous processing is extremely useful.

It can provide:

  • Better request latency
  • Independent scaling
  • Traffic buffering
  • Decoupling between components
  • More resilient processing pipelines
  • Better utilization of worker capacity

But those benefits come with a different failure model.

When you introduce a queue, you introduce time.

When you introduce time, you introduce intermediate states.

When you introduce intermediate states, you need to define how the system behaves when something fails between them.

That is the real architectural trade-off.

15. When Asynchronous Processing Makes Sense

I would consider asynchronous processing when:

  • The caller doesn’t need the final result immediately
  • Work can safely happen later
  • Traffic arrives in bursts
  • Processing can be separated from request handling
  • Workers need to scale independently
  • Temporary downstream failures should be absorbed
  • The business process naturally consists of stages

But I would be cautious when:

  • The caller requires an authoritative result immediately
  • Ordering is critical and difficult to guarantee
  • The operation has irreversible side effects
  • The business cannot tolerate eventual consistency
  • The team has limited observability
  • Retry and recovery semantics haven’t been designed
  • Nobody can clearly explain what happens when a worker crashes halfway through processing

That last point is particularly important.

16. The Question I Ask Now

When someone proposes:

“Let’s make this asynchronous.”

I don’t immediately ask which messaging platform we should use.

I ask:

“What happens if the consumer processes the message and crashes before acknowledging it?”

Then:

“What happens if the message is delivered twice?”

Then:

“What happens if the consumer succeeds but the downstream system times out?”

And:

“How do we know what actually happened?”

If those questions don’t have clear answers, the architecture isn’t ready.

The technology choice is secondary.

The failure model comes first.

17. The Architectural Lesson

Asynchronous processing didn’t make our system less reliable because queues are unreliable.

It made the system harder to reason about because we had changed the consistency and failure model.

We had traded:

Simple request-response

for:

Distributed workflow

That trade-off was worthwhile.

But only after we treated reliability as part of the design rather than something the messaging infrastructure would provide automatically.

The important lesson is:

Asynchronous processing doesn’t remove complexity. It moves complexity into retries, state transitions, ordering, consistency, and recovery.

A queue can absorb traffic.

It cannot decide what correctness means.

A message broker can deliver events.

It cannot make a non-idempotent operation safe.

A worker can retry.

It cannot determine whether the previous attempt already changed the business state.

Those decisions belong in the architecture.

Final Thought

The best asynchronous systems aren’t the ones with the most queues, events, or consumers.

They are the ones where the failure semantics are understood.

Before introducing asynchronous processing, ask:

What work can safely happen later?

What does success mean?

Can the operation happen twice?

Does ordering matter?

Where is consistency required?

What happens when the worker crashes halfway through?

How does the system recover?

If those answers are clear, asynchronous processing can be a powerful architectural tool.

If they aren’t, the queue may make the system faster while quietly making it harder to trust.

Performance is not the same thing as reliability.

And in distributed systems, making something asynchronous often means you need to design reliability more carefully, not less.

Leave a Reply

Your email address will not be published. Required fields are marked *