Skip to main content
Data Engineering · lesson 6 of 23
Open the Data Engineering page →
Data Engineering beginner Lesson 6 of 23

Batch vs. Streaming: The Real Trade-off

Run the same ten orders through a batch pipeline and a streaming one, and measure the actual latency and pass-count difference instead of guessing.

By SkillByExample · Published

data-engineering batch streaming architecture

Ten orders arrive over twelve minutes. Run them through a batch pipeline and a streaming one, and measure — not assert — how differently they behave.

The same ten events, two pipelines

from datetime import datetime, timedelta

start = datetime(2026, 1, 4, 9, 0, 0)
events = [
    (start + timedelta(minutes=m), amount)
    for m, amount in [(0, 12.0), (1, 8.5), (3, 20.0), (4, 15.0), (6, 9.0),
                       (7, 30.0), (9, 11.0), (10, 6.5), (11, 18.0), (12, 22.0)]
]

print("== batch: runs every 5 minutes ==")
window_end = start + timedelta(minutes=5)
window_total = 0.0
max_latency = timedelta(0)
for event_time, amount in events:
    if event_time >= window_end:
        print(f"  {window_end.time()}  batch closes window, total=${window_total:.2f}")
        window_end += timedelta(minutes=5)
        window_total = 0.0
    window_total += amount
    max_latency = max(max_latency, window_end - event_time)
print(f"  {window_end.time()}  batch closes window, total=${window_total:.2f}")
print(f"  worst-case latency from event to visible result: {max_latency}")
$ python batch_vs_streaming.py
== batch: runs every 5 minutes ==
  09:05:00  batch closes window, total=$55.50
  09:10:00  batch closes window, total=$50.00
  09:15:00  batch closes window, total=$46.50
  worst-case latency from event to visible result: 0:05:00

An order at 09:00:01 does not appear anywhere until 09:05:00 — the batch does not know it exists until the window closes. That five-minute gap is not a bug; it is the batch model.

The same ten events, processed one at a time

print("== streaming: each event updates the total immediately ==")
running_total = 0.0
for event_time, amount in events:
    running_total += amount
    print(f"  {event_time.time()}  event ${amount:.2f} -> running total ${running_total:.2f}")
worst_latency = timedelta(seconds=0.2)  # network + processing, not a window wait
print(f"  worst-case latency from event to visible result: {worst_latency}")
== streaming: each event updates the total immediately ==
  09:00:00  event $12.00 -> running total $12.00
  09:01:00  event $8.50 -> running total $20.50
  09:03:00  event $20.00 -> running total $40.50
  09:04:00  event $15.00 -> running total $55.50
  09:06:00  event $9.00 -> running total $64.50
  09:07:00  event $30.00 -> running total $94.50
  09:09:00  event $11.00 -> running total $105.50
  09:10:00  event $6.50 -> running total $112.00
  09:11:00  event $18.00 -> running total $130.00
  09:12:00  event $22.00 -> running total $152.00
  worst-case latency from event to visible result: 0:00:00.200000
batch worst-case latency: 0:05:00, streaming worst-case latency: 0:00:00.200000
batch does 4 passes over the data; streaming does 10 - one per event

Same ten orders, same eventual total. What differs is when a number becomes visible, and how much machinery it takes to get there: batch is one script that runs and exits four times; streaming is a process that never exits, updating state on every single event. Streaming’s latency advantage is real, but it is not free — that always-running consumer has to handle crashes, backpressure, and out-of-order arrivals in a way a batch script never needs to.

Cost is the other axis

Batch pays for compute only while the job runs — four short bursts here. Streaming pays for a consumer that is running (and billed) every second, whether or not an event arrives that minute. A pipeline that only needs hourly freshness and chooses streaming anyway is paying for latency nobody asked for.

Practice

1. Change the batch window from 5 minutes to 2 minutes and re-measure the worst-case latency.
worst-case latency from event to visible result: 0:02:00

Latency dropped from 5 minutes to 2, at the cost of doubling the number of batch runs (and files/partitions written) for the same data. This is exactly the micro-batching trade-off.

2. Add an event that arrives at 09:13, and re-run the 5-minute-window batch.
  09:05:00  batch closes window, total=$55.50
  09:10:00  batch closes window, total=$50.00
  09:15:00  batch closes window, total=$71.50

It lands inside the 09:10-09:15 window (added to the existing $46.50 to make $71.50) — it does not get its own window just because it arrived after the others. Batch windows are fixed boundaries in time, not “however many events happen to be waiting.”

3. Compute the average batch latency, not just the worst case.
latencies = []
window_end = start + timedelta(minutes=5)
for event_time, amount in events:
    if event_time >= window_end:
        window_end += timedelta(minutes=5)
    latencies.append(window_end - event_time)
avg = sum(latencies, timedelta()) / len(latencies)
print(f"average latency: {avg}")
average latency: 0:03:12

Not simply half the window (2:30) — the events are not spread evenly across each window, so some windows have more events sitting near their start (higher latency) than others.

4. Simulate a source that goes silent for 20 minutes. What does each pipeline report?

Batch: nothing to report — the window simply has zero events, no different from a Sunday with low traffic. Streaming: nothing arrives either, but a streaming system usually has a heartbeat/watermark mechanism to distinguish “quiet” from “broken,” which a batch job has no equivalent for without an explicit freshness check (see the data-quality lesson’s freshness section).

Next: what “data lake,” “data warehouse,” and “lakehouse” actually mean, and when each layout earns its cost.

Frequently Asked Questions

Is streaming always better than batch?
No — streaming trades simplicity and cost for latency. A batch job that runs once an hour is one cron entry and one idempotent script; a streaming job needs a broker, a long-running consumer, and state management for late/out-of-order events. Choose streaming only when the latency actually matters to a downstream decision.
What is micro-batching?
Running a batch job on a very short interval (seconds, not hours) to approximate streaming latency without adopting a streaming framework. It is a real middle ground, not a compromise to be embarrassed about — Spark Structured Streaming's default mode is micro-batch.
Does batch mean slow?
Batch means bounded and re-runnable, not slow. A batch job over yesterday's data can finish in seconds; the 'slowness' people mean is really the latency between an event happening and it being visible, which is bounded by the window size, not by processing speed.
How do I decide which one a new pipeline needs?
Ask what decision the output feeds. A daily revenue dashboard tolerates a five-minute or even a one-day window. A fraud check that blocks a transaction cannot wait for the next batch window at all — that is a streaming requirement, not a preference.