Aligned versus unaligned checkpoints under backpressure (optional, needs a local stream engine)
Simulate a modelNeeds Python
Problem
A stream engine takes a consistent checkpoint by injecting barriers into the stream. An aligned barrier travels in order behind the records already in flight, so the checkpoint completes only when everything buffered before it has been processed; under backpressure (a slow sink with full buffers) that can take a long time, and a checkpoint that cannot complete means the job cannot recover from a recent point. An unaligned barrier overtakes the buffered records and stores them in the checkpoint instead: it completes quickly and is larger.
The chapter's lab runs a local stream engine with a deliberately slow sink. This run uses a model instead, not a real engine: a source, a network buffer of 20,000 records with backpressure, and a sink whose rate goes from 2,000 down to 100 records per second, with a checkpoint every 10 seconds. Checkpoint duration and size, aligned and unaligned.
The question. How do checkpoint duration and size change as the sink slows, aligned and unaligned?
Predict
With aligned checkpoints, checkpoint duration grows when the sink slows down. With unaligned checkpoints, the duration stays short and the checkpoint size grows (it now contains in-flight records).
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
aligned-versus-unaligned-checkpoints.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Aligned against unaligned checkpoints (a model). Not a real stream engine: a tick-by-tick model of a source, a network buffer of bounded size (backpressure) and a sink, with checkpoint barriers injected every 10 seconds. An aligned barrier travels in order behind the records already buffered, so the checkpoint completes when the sink has drained them; an unaligned barrier overtakes the buffer and the checkpoint stores the buffered (in-flight) records instead. Checkpoint duration and size as the sink slows down. """ import argparse, json from collections import deque p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--source-rate", type=int, default=1000, help="Records per second the source can emit.") p.add_argument("--sink-rates", default="2000,1000,500,250,100", help="Records per second the sink can write.") p.add_argument("--buffer", type=int, default=20_000, help="In-flight records the network buffers hold.") p.add_argument("--state-kb", type=float, default=512, help="Operator state size (KB).") p.add_argument("--record-bytes", type=int, default=200, help="Bytes per in-flight record.") p.add_argument("--seconds", type=int, default=300, help="Simulated seconds.") p.add_argument("--interval", type=int, default=10, help="Checkpoint interval (s).") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() TICK = 10 # ticks per second def sim(sink_rate, unaligned): q = deque(); durations, sizes = [], []; pending = [] # pending: (inject time, records ahead) src, snk = args.source_rate / TICK, sink_rate / TICK; acc_src = acc_snk = 0.0 for tick in range(args.seconds * TICK): t = tick / TICK if tick % (args.interval * TICK) == 0 and tick: if unaligned: # overtake: snapshot the buffered records, done at once durations.append(1 / TICK); sizes.append(args.state_kb + len(q) * args.record_bytes / 1024) else: pending.append([t, len(q)]) # wait behind everything already buffered acc_src += src while acc_src >= 1 and len(q) < args.buffer: q.append(1); acc_src -= 1 acc_src = min(acc_src, 1.0) # backpressure: the source waits acc_snk += snk; drained = 0 while acc_snk >= 1 and q: q.popleft(); acc_snk -= 1; drained += 1 acc_snk = min(acc_snk, 1.0) for p_ in pending: p_[1] -= drained for p_ in [p_ for p_ in pending if p_[1] <= 0]: durations.append(t + 1 / TICK - p_[0]); sizes.append(args.state_kb); pending.remove(p_) tail = durations[len(durations) // 2:] or [float("nan")]; stail = sizes[len(sizes) // 2:] or [float("nan")] return sum(tail) / len(tail), sum(stail) / len(stail), len(durations) rows = [] for s in map(int, args.sink_rates.split(",")): for unaligned in (False, True): d, size, n = sim(s, unaligned) rows.append({"sink_rate": s, "mode": "unaligned" if unaligned else "aligned", "duration_s": d, "size_kb": size, "completed": n}) for r in rows: print(f"sink {r['sink_rate']:>5}/s {r['mode']:<9} checkpoint takes {r['duration_s']:6.1f} s, size {r['size_kb']:8.0f} KB ({r['completed']} completed)") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"checkpoints": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run aligned-versus-unaligned-checkpoints.py uv run aligned-versus-unaligned-checkpoints.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --source-rate1000 | Records per second the source can emit. |
| --sink-rates2000, 1000, 500, 250, 100 | Records per second the sink can write. |
| --buffer20,000 | In-flight records the network buffers hold. |
| --state-kb512 | Operator state size (KB). |
| --record-bytes200 | Bytes per in-flight record. |
| --seconds300 | Simulated seconds. |
| --interval10 | Checkpoint interval (s). |
Our run
Data table
| Sink rate (records/s) | series | Checkpoint duration (s) |
|---|---|---|
| 2,000 | aligned | 0.1 |
| 2,000 | unaligned | 0.1 |
| 1,000 | aligned | 0.1 |
| 1,000 | unaligned | 0.1 |
| 500 | aligned | 39.9 |
| 500 | unaligned | 0.1 |
| 250 | aligned | 79.9 |
| 250 | unaligned | 0.1 |
| 100 | aligned | 200 |
| 100 | unaligned | 0.1 |
Duration and size
| Sink (/s) | Mode | Duration | Size (KB) | Completed in 300 s |
|---|---|---|---|---|
| 2,000 | aligned | 100 ms | 512 | 29 |
| 2,000 | unaligned | 100 ms | 512 | 29 |
| 1,000 | aligned | 100 ms | 512 | 29 |
| 1,000 | unaligned | 100 ms | 512 | 29 |
| 500 | aligned | 39.9 s | 512 | 26 |
| 500 | unaligned | 100 ms | 4.41K | 29 |
| 250 | aligned | 79.9 s | 512 | 22 |
| 250 | unaligned | 100 ms | 4.41K | 29 |
| 100 | aligned | 200 s | 512 | 10 |
| 100 | unaligned | 100 ms | 4.42K | 29 |
sink 2000/s aligned checkpoint takes 0.1 s, size 512 KB (29 completed)
sink 2000/s unaligned checkpoint takes 0.1 s, size 512 KB (29 completed)
sink 1000/s aligned checkpoint takes 0.1 s, size 512 KB (29 completed)
sink 1000/s unaligned checkpoint takes 0.1 s, size 512 KB (29 completed)
sink 500/s aligned checkpoint takes 39.9 s, size 512 KB (26 completed)
sink 500/s unaligned checkpoint takes 0.1 s, size 4408 KB (29 completed)
sink 250/s aligned checkpoint takes 79.9 s, size 512 KB (22 completed)
sink 250/s unaligned checkpoint takes 0.1 s, size 4413 KB (29 completed)
sink 100/s aligned checkpoint takes 199.9 s, size 512 KB (10 completed)
sink 100/s unaligned checkpoint takes 0.1 s, size 4416 KB (29 completed)What we found
While the sink kept up, both modes took 100 ms. Once it fell behind and the buffer filled, aligned checkpoints took the time to drain the buffer: 39.9 s at 500 per second and 200 s at 100, so only 10 completed in five minutes. Unaligned checkpoints stayed at 100 ms and grew from 512 KB to 4.42K KB, the buffered records they now carry.
Read it
Under backpressure an aligned checkpoint waits for the very backlog that caused the backpressure, so the job's recovery point falls further behind exactly when it is struggling. Unaligned checkpoints trade checkpoint size for checkpoint time; bounding the in-flight buffers bounds both.