Back to Intelligence Insider

Intelligence Insider

Streaming Is Overrated Until It Isn't

I have a confession: we don't run Kafka. Every architecture diagram on the internet has Kafka in it. Ours doesn't. The product works fine. Somewhere along the…

Data Team

I have a confession: we don't run Kafka. Every architecture diagram on the internet has Kafka in it. Ours doesn't. The product works fine.

Somewhere along the way, "event-driven" stopped being an architectural decision and became a personality trait. It shows up on whiteboards before anyone has established that a stream of events is the right model for the problem. It's in the diagram because it's in every other diagram. And so I'll say the quiet part out loud: streaming infrastructure - Kafka, Pulsar, Kinesis - is the most prematurely-adopted technology in B2B SaaS architecture. For the overwhelming majority of workloads, a FastAPI BackgroundTask and a scheduled batch job do the same job with a fraction of the operational surface area. And more often than teams like to admit, the streaming layer becomes the source of more bugs than the problem it was brought in to solve.

Let me make the case.

The two taxes you pay before you get any benefit

Premature streaming fails in two predictable ways, and you start paying for both on day one - long before you see any upside.

The complexity tax. Every broker you stand up is a new distributed system sitting in the critical path of your application. That's more code, more failure modes, more things that page you at 3am. Consumer groups, partition rebalancing, dead-letter queues, schema registries, offset management, backpressure, poison-message handling - none of that is your product. It's the machinery you now have to operate in order to run your product. You didn't add a message bus; you adopted a second stateful system with worse ergonomics and a steeper learning curve, and now every engineer on the team has to hold it in their head before they can safely change anything.

The consistency tax. This is the subtle one, and it's the one that actually hurts. The moment you go event-driven, you inherit the hardest problems in distributed computing whether or not your business needs them. Event ordering. Exactly-once delivery. Idempotency. "At least once" delivery - which is what you actually get - means every single consumer must be idempotent, or the day a redelivery happens you double-charge a customer or send a notification twice. Ordering guarantees evaporate the instant you add a second partition. Reasoning about "what is the current state of this entity" becomes an exercise in replaying a log rather than reading a row. None of this is Kafka's fault; Kafka is excellent at what it does. It's the tax on the paradigm. You pay it in full the moment you adopt the paradigm, and you pay it forever.

Here's the trap: both taxes are invisible in the demo. Everything works when there's one consumer, one partition, and no load. The bill arrives later - in the on-call rotation, in the incident that took a day to trace to a rebalance, in the reconciliation job you had to write because "exactly once" wasn't.

When streaming actually earns its keep

To be clear: streaming is not a mistake. It's a mistake before you need it. There are three signals that justify the infrastructure, and the reason they're useful is that they're specific - you can point at them.

  1. Sub-second latency on cross-system effects. When an event in one system must produce a visible effect in another system in well under a second, sustained, and neither a synchronous call nor a short batch cycle can meet that bar - a stream is the right tool. Fraud signals, live pricing, real-time inventory across channels. Name the latency budget; if it's genuinely sub-second and cross-system, you've earned it.

  2. Multi-consumer fanout. When one event legitimately needs to be consumed by many independent downstream systems, each moving at its own pace, each able to replay from its own position without affecting the others - that's exactly what a log-based broker is for. The key word is many and independent. Two consumers isn't fanout; it's two function calls.

  3. A durable, replayable log for compliance. When you have a regulatory or audit requirement to reconstruct exactly what happened, in what order, and to prove it - an append-only, immutable log is the correct primitive, not a bolt-on. If the auditor is going to ask you to replay history, build history you can replay.

If one of those is a real, named requirement in front of you - not a hypothetical someone raised in a design review - buy the ticket. Streaming is worth it, and doing it well is a competitive advantage.

The pattern that covers 80% of cases

Here's the shape most teams actually need: request → background task → eventual consistency.

A request comes in. You do the synchronous part - validate, persist, return a fast response. The slow or cross-system work gets handed off to a background task. State converges a moment later. The user gets their 200; the side effects settle behind them. That's it. That single pattern absorbs the vast majority of "we need to do something after this happens" requirements without a broker anywhere in sight.

In our stack that's FastAPI BackgroundTasks for anything that can run in-process right after the response ships. For work that has to happen on a schedule - nightly syncs, digest sends, model retraining - it's a scheduler job guarded by a PostgreSQL advisory lock, so that no matter how many app instances are running, exactly one of them fires the cron. The database we already operate becomes the coordination layer. No separate worker fleet, no Redis pretending to be a queue.

Receipts, since I promised them:

  • Model training triggers run as FastAPI BackgroundTasks. A write lands, retraining kicks off after the response returns, the user never waits on it.

  • Inbound email as a channel arrives over a Resend webhook - an HTTP POST we already know how to authenticate, log, and retry. There's no stream to consume; there's an endpoint.

  • Cluster-safe scheduling is a PostgreSQL advisory lock wrapped around a scheduler job. However many instances we run, exactly one fires the job. Postgres - which we're already running and already back up - is the lock.

No Kafka. No Celery. No Redis-as-queue. It works at the scale we need, and - this is the part that actually matters - it fits in one engineer's head.

How to migrate when you actually need it

The strongest argument for starting simple is that the exit is clean if you build the seam correctly. Put your side effects behind a thin interface from day one - a single "emit this event" call. Today, that call enqueues a background task or writes a row. The day one of the three signals becomes real, you swap the implementation behind that interface for a producer that writes to a topic. Your call sites don't change.

You've deferred the complexity until you have the load, the team, and the concrete requirement to justify it - and you've earned the right to design the streaming layer around a problem you actually understand, instead of one you imagined in a meeting. Make the decision reversible, then defer it. That's the whole trick.

"But you'll need it eventually"

Sure. Maybe. And shipping six quarters earlier without it is still the right call.

The eventually argument proves too much. By that logic you'd adopt every technology you might one day need on day one, and you'd never ship anything. Engineering is the discipline of solving the problem in front of you well enough that you earn the right to solve the next one. Provisioning for a scale you don't have, with a team you don't have, against requirements you can't yet name, isn't foresight - it's a very expensive form of procrastination dressed up as diligence.

A simple test

Here's the test I use. If you can name the specific business event that needs sub-second, cross-system fanout - the actual event, the actual systems, the actual latency budget - streaming is justified. Go build it, and build it well.

If you can't name it, you're not making an architecture decision. You're doing resume-driven development. And the customers waiting on the features you didn't ship don't care what's on your diagram.


#SoftwareArchitecture #EngineeringLeadership #SaaS #SystemDesign #StartupEngineering #DistributedSystems #TechDebt