Ubin.io All Articles
Infrastructure & DevOps

You Think It's Async. Your System Disagrees.

By Ubin.io Infrastructure & DevOps
You Think It's Async. Your System Disagrees.

There's a particular kind of confidence that sets in after you've wired up Kafka, dropped a few SQS queues into your stack, and replaced your REST calls with event publishers. It feels like you've leveled up. Decoupled. Resilient. Truly asynchronous.

And then Black Friday hits, or your user base doubles overnight, or some upstream service hiccups — and suddenly your beautiful event-driven system is processing one message at a time like it's 2009 running on a single-threaded cron job.

Welcome to the async trap. A lot of teams fall into it. Most don't realize it until something breaks.

The Illusion of Parallelism

Here's the uncomfortable truth: slapping a message queue between two services doesn't automatically make your system asynchronous in any meaningful way. It makes it look asynchronous. The queue is real. The decoupling is real. But if your consumer is reading messages one at a time, waiting for a database write to confirm before pulling the next event, you haven't escaped synchronous processing — you've just moved it downstream and added latency on top.

This pattern shows up constantly in systems that were designed with good intentions. A developer reads the docs, sets up a consumer group, and thinks, "Great, Kafka will handle the parallelism." But Kafka doesn't parallelize anything on its own. Partitions determine your ceiling. Consumer configuration determines your floor. And if your consumer code is doing blocking I/O inside a single-threaded loop, you've built a very elaborate synchronous pipeline with extra steps.

Where the Bottlenecks Actually Hide

Let's get specific, because the devil is in the implementation details.

Single-partition queues. This one's embarrassingly common. You spin up an SQS queue or a Kafka topic with a single partition, and then wonder why throughput caps out at a few hundred messages per second. Parallelism in most queue systems is bounded by partition count. One partition means one consumer doing real work at a time, regardless of how many consumers you've spun up.

Synchronous side effects inside async handlers. Your event handler fires asynchronously — great. But inside that handler, you're calling a third-party API synchronously, waiting for a response, then writing to a relational database, then sending a confirmation email through an SMTP client. Every one of those operations is blocking. You've hidden a waterfall inside an async wrapper and called it modern architecture.

Database serialization. Row-level locking, sequence generation, and upsert logic are notorious for turning parallel consumers into a queue of their own. If ten consumers are all trying to update the same user record based on incoming events, the database becomes the real bottleneck — and it will serialize them for you whether you planned for it or not.

Ordered processing requirements. Sometimes the serialization is intentional but under-examined. Teams that need to process events in strict order often implement consumer-side locking or single-threaded processing as a quick fix. The ordering guarantee is real and sometimes necessary — but if you haven't thought carefully about which events actually need ordering versus which ones you're serializing out of habit, you're leaving parallelism on the table.

The Debugging Problem

What makes this particularly frustrating is that these bottlenecks don't announce themselves. Your queue depth looks fine. Your consumer lag looks manageable. Your dashboards show green. But end-to-end latency keeps creeping up, and you can't figure out why.

A few places to look:

Trace across the full event lifecycle. Distributed tracing tools like Jaeger or Honeycomb let you follow a single event from publish to final state. If you see long gaps between consumer receipt and processing completion, that's a signal. If those gaps correlate with database write times or external API calls, you've found your culprit.

Measure consumer concurrency, not just consumer count. The number of consumer instances running is not the same as the number of operations executing in parallel. Instrument your handlers to track active concurrent executions. If that number rarely exceeds one or two even under load, your concurrency model isn't doing what you think it is.

Look at your lock contention metrics. Most databases expose lock wait times and deadlock counts. If those numbers spike during high-throughput event processing, your async system is being throttled by synchronous database behavior.

Profile your handlers in isolation. Spin up a consumer in a staging environment, hammer it with synthetic events, and measure wall-clock time per message. Then look at what percentage of that time is spent waiting versus computing. If it's mostly waiting, your blocking calls are the story.

Architectural Red Flags to Watch For

If you're doing a design review and you see any of these patterns, it's worth pushing back before they hit production.

None of these are automatic dealbreakers. Sometimes you genuinely need strict ordering. Sometimes an external API call is unavoidable. But each of these patterns should prompt a conversation: Is this bottleneck intentional? Do we understand the throughput ceiling it creates? Have we tested what happens when that ceiling gets hit?

Fixing It Without Burning Everything Down

The good news is that most of these issues are fixable incrementally. You don't need to rebuild your event architecture from scratch.

Start by increasing partition count on your highest-volume topics and ensuring your consumer group is scaled to match. This alone can unlock significant parallelism if your handlers are otherwise clean.

For blocking I/O inside handlers, look at async I/O libraries appropriate for your runtime. Node.js developers have native async/await patterns that can help. Python teams might reach for asyncio or thread pools. JVM shops might consider reactive frameworks like Project Reactor.

For database contention, consider whether you actually need to write synchronously inside the handler. Can you batch writes? Can you publish a downstream event and let a separate process handle persistence? Sometimes the right move is adding another layer of decoupling instead of fighting the database.

The Honest Reckoning

Async architecture is genuinely powerful — but power requires precision. Kafka and SQS and EventBridge are exceptional tools, and they can absolutely deliver the parallelism and resilience they promise. But they won't save you from synchronous thinking baked into your application logic.

The teams that get this right aren't the ones with the most sophisticated tooling. They're the ones who understand exactly where their system is doing work in parallel and where it's waiting in line — and they've made conscious decisions about every single one of those tradeoffs.

If you haven't audited your event pipelines for hidden synchronous behavior, now is a better time than your next incident postmortem.