Skip to content
About
Main content

Aligned versus unaligned checkpoints under backpressure (optional, needs a local stream engine)

Simulate a modelNeeds Python

Problem

A stream engine takes a consistent by injecting into the stream. An travels in order behind the records already in flight, so the checkpoint completes only when everything buffered before it has been processed; under (a slow 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 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 the source at 1,000 records a second and the sink at 100, how long does an aligned checkpoint take?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    aligned-versus-unaligned-checkpoints.py62 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat 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

Checkpoint duration as the sink slowsApple M2 · 8 cores · 8 GB
alignedunaligned
Data table
Checkpoint duration as the sink slows
Sink rate (records/s)seriesCheckpoint duration (s)
2,000aligned0.1
2,000unaligned0.1
1,000aligned0.1
1,000unaligned0.1
500aligned39.9
500unaligned0.1
250aligned79.9
250unaligned0.1
100aligned200
100unaligned0.1
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

Duration and size

Duration and size
Sink (/s)ModeDurationSize (KB)Completed in 300 s
2,000aligned100 ms51229
2,000unaligned100 ms51229
1,000aligned100 ms51229
1,000unaligned100 ms51229
500aligned39.9 s51226
500unaligned100 ms4.41K29
250aligned79.9 s51222
250unaligned100 ms4.41K29
100aligned200 s51210
100unaligned100 ms4.42K29
Output · uv run aligned-versus-unaligned-checkpoints.py
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.