Data Engineering · lesson 6 of 23
Beginner
8- 1 Data Engineering Fundamentals
- 2 File Formats: CSV, JSON, Parquet
- 3 Idempotency and Safe Retries
- 4 Batch Pipelines and Time Windows
- 5 Partitioning and Physical Layout
- 6 Batch vs. Streaming: The Real Trade-off
- 7 Data Lake vs. Data Warehouse vs. Lakehouse
- 8 Data Modeling Basics: Star Schemas and Denormalization
Intermediate
4Advanced
11- 13 Streaming: Exactly-Once Semantics
- 14 Schema Evolution and Data Contracts
- 15 Streaming Architecture: Lambda and Kappa
- 16 Data Lineage: Tracing a Column Back to Its Source
- 17 Data Governance: Catalogs, Classification, and Access Policy
- 18 Data Security and PII: Hashing, Masking, and Tokenization
- 19 Pipeline Observability: Tracing, Anomaly Detection, and Alerting
- 20 Cost Optimization: Measuring What Partition and Column Pruning Actually Save
- 21 CI/CD for Data: Gating Promotion with Real Tests
- 22 Disaster Recovery: RPO, RTO, and What a Backup Actually Loses
- 23 Data Platform Architecture: Wiring the Pieces Together
Also part of Data Engineering
9- + Your First Airflow DAG from Airflow
- + Kafka Topics, Partitions, and Offsets from Kafka
- + Spark Architecture: Driver, Executors, and Cluster Managers from Apache Spark
- + Models, ref, and the DAG from dbt
- + Delta Lake: The Transaction Log from Databricks
- + The Medallion Architecture from Databricks
- + File I/O, pathlib, and JSON in Python from Python
- + Error Handling in Python from Python
- + Terraform Fundamentals (IaC) from Terraform
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
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.