Skip to main content
ETL Pipeline Design

Batch vs Stream Processing: Choosing When Your Data Won't Stop Flowing

So you're designing an ETL pipeline, and the data keeps coming. Maybe it's clickstream events from a mobile app, IoT sensor readings, or financial transactions. The old way—dump everything into a data warehouse once a night—feels too slow. But streaming adds complexity, cost, and new failure modes. This isn't a simple either-or choice. I've seen teams blow months building a streaming pipeline that could have been a batch job. And I've seen batch jobs fail to deliver on a real-time dashboard requirement. The trick is knowing which trade-offs bite you later. This guide lays out the field context, common patterns, and hard lessons—so you can decide with open eyes. Where the Batch vs Stream Decision Hits Real Work Latency requirements you can feel in the wallet I sat in a room last year with three teams arguing about a pipeline. Marketing needed fraud alerts in under three seconds.

So you're designing an ETL pipeline, and the data keeps coming. Maybe it's clickstream events from a mobile app, IoT sensor readings, or financial transactions. The old way—dump everything into a data warehouse once a night—feels too slow. But streaming adds complexity, cost, and new failure modes. This isn't a simple either-or choice.

I've seen teams blow months building a streaming pipeline that could have been a batch job. And I've seen batch jobs fail to deliver on a real-time dashboard requirement. The trick is knowing which trade-offs bite you later. This guide lays out the field context, common patterns, and hard lessons—so you can decide with open eyes.

Where the Batch vs Stream Decision Hits Real Work

Latency requirements you can feel in the wallet

I sat in a room last year with three teams arguing about a pipeline. Marketing needed fraud alerts in under three seconds. Engineering swore they could rebuild a batch job to run every ninety seconds. The database team just stared at the ceiling. That gap—between what latency your business actually pays for and what your architecture delivers—is where the batch-versus-stream decision hits real work. A batch window of fifteen minutes feels fine until a competitor's dashboard shows live conversion data and your execs ask why you're "behind." The trade-off is brutal: stream processing costs more in operations and tooling, but batch that runs too infrequently hides failures for hours.

The catch is that most teams overestimate their latency needs. I have seen a fintech startup insist on sub-second streaming for transaction logs—then discovered their downstream compliance team only reconciled at midnight anyway. They paid six figures for Kinesis when a nightly Spark job would have done the job. But flip it: clickstream analytics for a retailer during Black Friday? Batch windows collapse under the load. A thirty-minute delay means you can't react to a checkout bottleneck until after the damage is done. That hurts.

Clickstream, IoT, financial transactions—the three pressure tests

Clickstream data is the classic stream candidate because every second of latency is a lost optimization. You want to see where users drop off, what campaign triggers a purchase right now—batch just can't support that feedback loop. IoT is trickier. Temperature sensors in a warehouse can batch hourly without anyone dying; vibration sensors on a turbine can't. One client sent us fifty gigabytes of sensor data daily but only needed alerts for thresholds that required five consecutive outlier readings. That's a hybrid pattern, not a pure stream problem. They wasted money until we throttled the stream to only fire when the batch check on the moving window flagged a real anomaly.

Financial transactions sit in the middle. Most payments processing can tolerate a few seconds—the card network itself adds latency. But fraud detection needs stream-level inspection because a bad actor drains an account in forty seconds. We fixed this by running a lightweight streaming filter for known fraud patterns while a batch job recalibrated the model overnight. That combo avoids the anti-pattern of processing every normal transaction twice. The mistake teams make is assuming all transactions are equally urgent.

'The hardest part isn't choosing stream over batch—it's admitting that half your data doesn't care about the difference.'

— engineering lead, anonymous post-mortem on a reverted stream pipeline

Why this question bites harder now than five years ago

Cloud costs got weird. Streaming services like Kinesis and Pub/Sub charge per message even when idle. Batch on spot instances became absurdly cheap. Meanwhile, data volume exploded—a mid-size IoT deployment now outpaces what a single Spark cluster could handle in 2019. The calculus shifted: stream processing's operational tax used to be acceptable because hardware was the bottleneck. Now it's often cheaper to throw compute at a well-optimized batch job than to maintain a persistent stream infrastructure that must always be up. That said, the rise of real-time ML inference has pulled stream processing into places where batch never belonged. Teams that ignore this are building pipelines that will need to be torn out and rebuilt within two years. I have seen it happen three times now—always the same conversation in the room where the decision first got made.

Foundations People Get Wrong

Micro-batch vs true streaming: the spectrum

Most teams treat this as a binary. It isn't. Micro-batch sits in the muddy middle — Spark Structured Streaming at a five-second trigger is not the same animal as Kafka Streams processing each record as it lands. I have watched architecture reviews collapse because someone insisted "we're streaming now" while their pipeline still polled in six-second windows. The operational gap is real: a true streaming system tracks offsets per record, can react within milliseconds, and usually forces you to handle out-of-order data at the edge. Micro-batch gives you a comfort blanket — retry a failed batch, replay a five-minute window — but that blanket smothers latency. If your SLA reads "under ten seconds" and you deploy Spark with a thirty-second trigger, you're not streaming. You're batching with a fast clock. Call it what it's.

Event time vs processing time confusion

The single most expensive mistake in stream design. Event time is when the thing happened — a sensor read, a click, a payment authorization. Processing time is when your pipeline sees it. They diverge constantly. A mobile checkout on a subway — event time 14:03:12, processing time 14:07:48 because the train killed the connection. Naive pipelines stamp the processing time and call it done. Wrong order.

‘Late data is not anomalous — it's the normal state of any distributed system that touches unreliable networks.’

— overheard at a Kafka meetup, paraphrased from a production postmortem

That hurts because watermarking is hard. Set your watermark too tight and you drop a percent of real clicks. Set it too loose and your windowed aggregations lag by hours. Most teams skip this: they define event time in the schema but never actually enforce watermark policy in the stream processor. The result — silent data loss masked by a dashboard that looks fine until your finance team runs month-end reconciliation and the numbers don't rhyme. We fixed this once by tracking observed lag per partition and alerting when median drift exceeded 120 seconds. Same system, different survival rate.

Not every data checklist earns its ink.

Not every data checklist earns its ink.

Exactly-once semantics and what they actually mean

Every vendor promises it. Few implementations deliver it without trade-offs. Exactly-once in a stream processor means the output of a transformation — say, a deduped join — is applied exactly once to the downstream state store. It does not mean your database won't see a duplicate row on retry unless the sink is idempotent. The pitfall is subtle: Kafka's exactly-once spans the produce-consume cycle within the broker, but your downstream PostgreSQL has no idea what a transaction ID is unless you build that idempotency key yourself. I have seen a team rewrite three months of stream logic because they assumed transactional guarantees propagated through a JDBC connector. They didn't.

The catch is cost. Exactly-once requires transactional coordination — Kafka transactions, two-phase commit, or idempotent sinks with dedup tables. That adds latency and complexity. For many use cases, at-least-once plus a dedup window at read time is cheaper to operate and easier to debug. The question is not "can you achieve exactly-once?" but "what are you willing to pay in throughput and operational pain to avoid a single 0.01% duplicate?" Usually — nothing. Most teams over-engineer here. They chase a guarantee they don't need because the marketing slide said it was table stakes.

Patterns That Usually Work

Lambda architecture: batch + stream with caution

Lambda sounds reasonable on paper. You run a fast stream layer for low-latency results and a batch layer that corrects everything overnight. The problem? You maintain two codebases. Three if counting the serving layer that merges outputs. I have watched teams spend six months stitching Lambda together, only to discover that the merging logic itself generates more bugs than the original pipelines. The stream gives you 5-minute approximations; the batch gives you truth. But when those two numbers diverge by 11%, nobody trusts either one. The pattern that actually survives requires a strict SLA: the batch layer must override stream output entirely, not merge. Use Lambda only when absolute correctness matters more than team velocity, and you have a dedicated ops person for the merge seam. Most orgs don't.

Kappa architecture: pure stream with reprocessing

Kappa ditches the batch layer. Everything is a stream, and reprocessing means replaying events from a durable log. That sounds clean until you hit a schema change that broke your Kafka topic backwards. The trick that works: keep your event schema versioned and your reprocessing window bounded — replaying six months of data because one field name changed is a Tuesday you don't want. Kappa excels when your business logic is simple enough to express in stateless transforms and your data volume stays under a few hundred gigabytes per day. Above that, the cost of recomputation starts hurting. One concrete pattern I have seen work: run Kappa for the hot path (sub-second alerts, live dashboards) and schedule a daily validation job that reads from the same topic but writes to a reconciliation table. No separate batch code, just a different consumer group with a different WHERE clause. That keeps the maintenance drift near zero.

Hybrid approaches that actually scale

Most successful pipelines I have encountered are neither pure Lambda nor pure Kappa — they're pragmatic hybrids hiding under operational names. The simplest: route events through a stream processor for immediate actions (fraud blocks, inventory deductions), then dump the same events into object storage for hourly or nightly batch transforms. The stream doesn't try to produce final tables; it just triggers side effects. The batch process treats the raw events as immutable source of truth and rebuilds aggregates from scratch. No merge logic, no offset reconciliation. Another pattern that holds up: use stream processing for windowed aggregations (last hour, last day) and batch for full-historical recalculations that overwrite those rolled-up tables. The catch is your team must accept that stream and batch outputs will differ slightly — and the batch output is the canonical one. If your business users can't tolerate a 3% swing between real-time and end-of-day numbers, don't hybridize. Run batch only and buy shorter scheduling intervals. What usually breaks first is the handoff timestamp: if your stream processor uses event time and your batch processor picks up ingestion time, the seam blows out. Standardize on one clock, and keep a manual reprocess button for the inevitable drift.

One more pattern worth stealing: treat your stream processor as the suggestion layer and batch as the authority layer. A fraud detection system I worked on used Flink to flag suspicious transactions within 200 milliseconds — but no automatic block was executed until a five-minute batch job validated the pattern against historical lookups. Latency stayed under one minute, false positives dropped by 40%, and the ops team slept through the night.

Combine stream for speed and batch for trust — but never let the fast path make irreversible decisions without a slow path review.

— operational rule from a team running 50 GB/day event logs

Anti-Patterns That Make Teams Revert to Batch

Streaming everything without reason

The easiest way to kill a streaming pipeline is to stream data that never needed to move fast. I have watched teams wrap Kafka around daily CRM exports — 200 MB files that land once at 3 AM — and then spend six weeks debugging offset commits for data that nobody reads until noon. The cost is not just compute; it's cognitive load. Every developer now has to think about consumer groups, schema registry compatibility, and replay windows for something a cron job and a COPY INTO statement could handle in twenty minutes. That sounds fine until the third schema change breaks the stream and the batch fallback takes two days to rebuild. Why pay stream complexity for batch latency? If your data arrives in scheduled chunks and your dashboard refreshes hourly, a stream is theater — not architecture.

Ignoring backpressure and exactly-once cost

The catch is that most teams underestimate what exactly-once semantics actually cost. They enable idempotent writes, set enable.idempotence=true, and assume the job is done. Then the downstream database starts choking under retries and the pipeline silently duplicates three million events. I have debugged this exact pattern: a Spark Structured Streaming job writing to Postgres with exactly-once sinks, except the DB connection pool exhausted because every micro-batch triggered duplicate detection scans. The team reverted to batch within a sprint. The lesson: exactly-once is a distributed systems lie unless you count the operational tax — it pushes complexity to the sink, and most sinks (Postgres, S3, Elasticsearch) charge per retry.

“Streaming that pretends backpressure doesn’t exist will eventually produce a batch pipeline — just one that fails unpredictably.”

— engineer who reverted three times in eighteen months

Most teams skip the backpressure audit. They assume Kafka brokers or Kinesis shards auto-magically scale. They don't. A sudden spike in traffic — say, Black Friday logs — floods the consumer, heap fills, GC pauses mount, and the checkpoint directory corrupts. Reverting to a nightly batch run feels like relief. That's the anti-pattern: treating throughput as capacity planning instead of a distributed system constraint.

Over-engineering before data volume justifies it

Wrong order. You pick streaming because the architecture is supposed to scale, then discover your event rate is 400 messages per minute — a volume any OLTP database could swallow with a simple trigger-based poller. I once worked on a pipeline where the team built Flink stateful aggregations for 2 MB of daily sensor data. The state backend was RocksDB. The checkpoint interval was one second. The result? A 12-node cluster running, idle, while a single cron job could have done the same calculation in 0.3 seconds. Not yet. That's the phrase I wish more teams heard. Streaming is a bet on future growth, but futures are written in present cash. When the VP asks why ops costs jumped 40% and latency barely changed, the revert is political, not technical. The pragmatic move: batch first, measure growth, migrate only when the batch window squeezes under SLA. Until then, keep the cron job. It's boring. It works. The seam blows out when you assume scale before you have the data to demand it.

Field note: data plans crack at handoff.

Field note: data plans crack at handoff.

Long-Term Maintenance and Drift Costs

Operational overhead of streaming infrastructure

Batch processing ages like a neglected shed — it gets dusty, the roof leaks, but the damn thing still stands. Stream processing ages like a race car driven daily on gravel roads. The first week is glorious. The second week you're replacing tires. By month six you have a full-time mechanic. I have watched teams burn two engineering months just keeping their Kafka cluster healthy — rebalancing partitions, chasing unclean leader elections, debugging consumer lag that spikes every Tuesday like clockwork. Batch has its own rot: cron jobs that silently fail, dependency chains that nobody remembers. But streaming's operational surface area is larger by an order of magnitude. You need a schema registry, a state store backup strategy, a plan for exactly-once semantics that actually works under load. That's not free. That's a headcount decision disguised as an architecture decision.

Schema evolution in stream vs batch

Batch handles schema changes with a shrug. You add a column, backfill last night's partition, run a job. Done. Stream handles schema changes like a cat walking through wet paint — it gets everywhere, you can't see the mess until something sticks, and then you have seven downstream systems all failing in different ways. The tricky bit is that streaming schemas are living contracts. Every consumer reads your Avro or Protobuf definition at a different moment. A field marked optional today was required yesterday. A record serialized at 3:02 PM can't be deserialized at 3:03 PM because the schema registry forward-evolution rules were configured wrong. Most teams skip this: testing schema evolution against historic data in a stream context. They test against the current topic, not against replayed data from three months ago. That hurts.

'Streaming schema drift is death by a thousand incompatible reads — each one tiny, the aggregate catastrophic.'

— observation after untangling a production incident where a deprecated enum field silently corrupted every downstream aggregation for nine weeks

Data quality monitoring differences

Batch quality checks are straightforward: count rows, check for nulls, validate referential integrity after each run. You have a checkpoint — the job finished or it didn't. Stream quality monitoring is an exercise in probabilistic anxiety. You can't check "every" record; you sample. You can't detect a silent drop in event quality until the dashboard shows numbers that feel wrong. By then the bad data has already propagated through three stateful joins. We fixed this by building a separate "quality stream" — a parallel pipeline that runs assertions on a 5% sample every thirty seconds, then alerts when anomaly thresholds break. The catch is that sample-based monitoring misses the worst kind of failures: the rare ones. A transformation bug that only triggers on Tuesdays at 14:00 UTC for mobile Android users running version 4.3? Your dashboard never sees it. Your customers do. The long-term cost is not the monitoring tooling — it's the cognitive drain of knowing your data might be wrong and not knowing how wrong. That uncertainty compounds. After eighteen months of streaming, I have never met a team that regrets investing in semantic monitoring over simple metric dashboards. The teams that skipped it? They're the ones quietly moving back to nightly batch reconciles.

When NOT to Use Stream Processing

Compliance-driven historical reports

Some data must sit still. Tax filings, audited revenue breakdowns, quarterly reconciliations—these demand a frozen snapshot, not a live feed. I have watched teams build beautiful stream processors only to discover that regulators require point-in-time reprocessing with deterministic lineage. The stream gives you continuous truth; compliance wants a certified, reproducibly perfect version of what the books looked like at midnight on March 31. That's a batch job. Or two. Or twelve, chained with checksums and signed manifest files. The catch is that streaming advocates often conflate low latency with correctness rigor. They push event-time windows and late-arrival thresholds—clever engineering that still can't produce the kind of locked, immutable archive an external auditor demands. When the question is not "how fast can we know" but "can we prove exactly what we knew on a specific date," batch wins. The trade-off is real: you sacrifice immediacy for a verifiable paper trail that survives any schema drift, any reprocessing pipeline change, any accidental double-publish from a misconfigured producer.

Small data volumes that don't need real-time

A few hundred patient intake forms per day. A cron job that pulls five vendor invoices at noon. That's not a stream; that's a trickle dressed up in Kafka attire. Honestly—I have seen startups spin up three-node clusters to process a CSV file that could fit in a spreadsheet cell. The infrastructure overhead alone—brokers, schema registries, consumer group rebalances, offset retention tuning—exceeds the entire data volume by orders of magnitude. Why? Because "streaming" sounded more modern on the architecture diagram. But the operational reality bites: you now maintain a state store for what amounts to a weekly flat file. Small-scale streaming introduces coordination complexity with zero latency benefit—your batch run finishes in fourteen seconds anyway. One concrete anecdote: a fintech team I advised was streaming 400 transactions per hour into Flink, then sinking results into a Postgres table that a dashboard queried every three hours. They were paying for real-time compute to serve near-real-time reads. We killed the stream, replaced it with a single INSERT ... SELECT cron job, and cut infrastructure costs by 70%. Not every data set needs to flow; some just need to sit still long enough to be useful.

'Streaming is not a goal. It's a tax you pay when batch can't deliver the latency your business actually requires.'

— engineer after unwinding a six-month stream implementation back to nightly batch runs

Immature teams without SRE support

This one stings because nobody admits it in the planning meeting. A strong batch pipeline can survive a junior engineer deploying a broken dependency—the next run either catches up or someone manually backfills. A stream pipeline doesn't forgive. A misconfigured consumer lag detection floods the operations channel at 3 AM. An unhandled deserialization error stalls the entire topology mid-afternoon Tuesday. The pitfall is subtle: streaming looks simpler in demos than it behaves under production pressure. Most teams skip this: planning for exactly-once semantics when your message broker is not idempotent, or handling backpressure when a downstream API takes 300 milliseconds instead of 50. I have seen three-person data teams spend entire sprints debugging checkpoint failures while their batch-using competitor shipped new dashboards. The editorial truth is hard: if your team has never run a distributed system in production at even moderate scale, start with batch. Batch is forgiving. Batch lets you sleep. You can stream later, after you have the operational muscle—monitoring, alerting, runbook culture—that prevents a single misrouted event from taking down your entire analytics layer. That sounds fine until your stream processes a null partition key and silently drops two hours of order data. And you don't notice until the morning standup. Not yet ready? Don't stream. Batch harder instead.

Open Questions and FAQ

Can you start batch and evolve to stream?

Yes—but the migration rarely looks how engineers expect. I have watched teams pile a stream processor on top of a nightly batch job and call it hybrid. That hurts. The seam between the two modes creates double write logic, timestamp drift, and debugging sessions that run past midnight. The cleaner path: start with a batch pipeline that writes immutable event logs (S3, Parquet, partitioned by hour). Then point a stream consumer at fresh partitions while backfilling old ones via batch. That works because the source of truth stays unchanged. You add stream, you don't replace batch. The catch is governance—two code paths that must agree on schema, deduplication, and exactly-once semantics. Most teams skip this: pick one mode for the core hop and treat the other as an append-only pressure test.

How to choose between Kafka, Kinesis, and cloud-native?

This question gets framed as a feature comparison. Wrong framing. The real filter is operational surface area. Kafka gives you unmatched replay, partition elasticity, and multi-team topology—but your on-call rotation now includes Kafka reassignment. Kinesis reduces ops by handing shard splitting to AWS, yet you trade away long retention and cross-region simplicity. Cloud-native services (Pub/Sub, Event Hubs, native queues) look tempting until you need rewind 72 hours for reprocessing—then you hit retention limits and feel cornered. I have seen a mid-size team burn three months tuning Kafka consumers for a pipeline that processed 200 events per second. A simple SQS-to-S3 bridge would have worked. The rule: pick Kafka when you need heterogeneous subscribers, custom partitioning, or replay beyond two weeks. Pick managed stream services when your team has zero Kafka experience and your latency requirement lives above 500 milliseconds. That sounds obvious. It never is when a vendor’s benchmark chart glows green.

What latency threshold justifies streaming?

Five seconds? Five minutes? The number that matters is your stakeholder's operational decision—not the dash. If a human refreshes a dashboard every ten minutes, a five-second stream buys you nothing but complexity cost. However, if downstream systems kick off notifications, charge consumer cards, or lock inventory thresholds, then even thirty seconds of delay causes revenue holes or compliance flags. I once watched a team stream every click event at sub-second latency because "real-time is the future." Their fraud model ran on hourly aggregates. The stream just queued clicks—no consumer cared. That's the anti-pattern: streaming because you can, not because a downstream process breaks without it. A better heuristic: ask "what fails if data arrives five minutes late?" If nothing fails, your pipeline tilts batch. If something crashes or mischarges, that latency ceiling is your irreducible number.

Odd bit about warehousing: the dull step fails first.

Odd bit about warehousing: the dull step fails first.

What about exactly-once semantics across batch and stream?

You don't get exactly-once across heterogeneous systems. Get comfortable with idempotency instead. Batch jobs can use watermark + upsert. Stream jobs can use transactional output on the sink. But writing once to Kafka and once to S3 through two different code paths—that seam will duplicate records or lose them. Common fix: assign a global deduplication ID (event UUID + timestamp window) at the ingest layer. Both modes check the dedup table before writing. That adds latency—another trade-off.

When should you give up on convergence entirely?

When your batch pipelines run for hours, not every hour. A transform that takes 90 minutes to finish can't sit beside a stream that processes events in 200 milliseconds—the integration point stalls. Most teams concede here: run batch for heavy enrichment, stream for light transforms, and bridge them with a final merge step overnight. That's not dirty. That's a different architecture entirely.

“We tried to make one pipeline rule all modes. What we actually built was a slow batch job with a Kafka skin.”

— lead data engineer at a payments scale-up, after reverting 70% of their event stream to daily loads

Your next experiment: pick a single pipeline that processes less than 1,000 events per second today. Run it batch for one week. Then run the same pipeline streamed—but don't optimize the stream path beyond one consumer group. Measure total engineering time, not throughput. The gap will tell you which mode your organization can actually sustain.

Summary and Next Experiments

Decision framework: latency, volume, team skill

Stop guessing. Draw a triangle with three axes: how fast does the business need answers, how much data actually arrives per second, and who will maintain whatever you build. Most teams fixate on volume first — that's a mistake. Latency tolerance should drive the call. If your marketing dashboard can survive a 15-minute delay, batch wins. If a fraudulent transaction needs blocking in under two seconds, you have no choice: stream it. The catch is that raw throughput rarely breaks batch pipelines — it's the spikey arrival pattern that does.

Skill sits underneath both axes. I have watched senior engineers burn weeks on exactly-once semantics for a use case that could have run fine on hourly cron jobs. That hurts. Real talk: if your team cannot explain checkpointing or backpressure within a few minutes of discussion, start with batch. Stream processing punishes gaps in distributed systems intuition. You can learn it — but not under a production deadline.

Run a small streaming pilot before committing

One concrete anecdote: a payment team I worked with spent six months redesigning their batch ETL into a full Kafka Streams topology. They hit offset-out-of-range errors for three weeks straight. The seam blew out because they never tested with real traffic patterns — only synthetic bursts. Don't do that. Pick one minor data product — something the business won't scream about if it degrades — and run a streaming pipeline alongside the existing batch one for two weeks. Measure both. Compare not just latency but also operational toil: how many alerts fired, how often did offsets lag, how many reprocessing runs were triggered?

That small experiment surfaces the real cost. Batch pipelines break in predictable, boring ways. Stream pipelines break at 3 AM with a cascading partition rebalance that nobody on call has debugged before. Honest teams discover this during the pilot, not the post-mortem.

'We thought stream processing would simplify the architecture. Instead it exposed every weakness in how we handled schema evolution and failure recovery.'

— Staff engineer at a fintech startup, reflecting on a six-month rewrite that was rolled back to batch

Measure and iterate

Set a hard threshold for your pilot. If end-to-end latency stays under your business requirement for four consecutive weeks with fewer incidents than the batch equivalent, proceed. If not — and this is important — revert immediately. The sunk cost fallacy kills more data architectures than wrong technology choices do. Maybe you learn that stream processing works for your real-time alerts but not for the aggregations that feed the monthly reports. That's fine. Run a hybrid. Many production systems I have seen do batch for the heavy lifting and stream for the hot path, connected by a bounded output table. The decision isn't permanent — but the drift cost gets worse the longer you delay the experiment.

Next step: pick your pilot. Now. Not next sprint. One stream, one table, one metric. Measure twice. Decide once.

Share this article:

Comments (0)

No comments yet. Be the first to comment!