Dedup window versus replay
Simulate a modelNeeds Python
Problem
Online deduplication remembers recent event ids for a while (a TTL) and drops repeats inside that window. That handles retries, which arrive seconds apart. It cannot handle a replay: re-processing last week's data after a bug fix sends events whose ids expired long ago, so they all look new. Replays need a different mechanism: replacement semantics, where the output for a period is overwritten as a whole, so running it twice gives the same result as running it once.
Build a consumer that dedups with an in-memory dictionary of event_id -> time and a 60-second TTL. Feed it 1,000 events, then replay the same 1,000 events 5 minutes later. Count how many are applied twice. Then change the sink to "overwrite the partition for this date" (idempotency by replacement) and repeat.
The question. How many replayed events does a 60-second dedup window catch, and what does overwriting the partition do?
Predict
The TTL-based dedup applies all 1,000 events a second time, because the keys have expired. Replacement semantics produce an identical result after the replay.
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
dedup-window-versus-replay.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Dedup window against replay. A consumer that dedups with event_id -> time and a 60-second TTL, fed 1,000 events and the same 1,000 again 5 minutes later; then a sink that overwrites the whole partition for the date (idempotency by replacement). """ import argparse, json p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--events", type=int, default=1000, help="Events.") p.add_argument("--ttl", type=float, default=60, help="Dedup TTL in seconds.") p.add_argument("--replay-after", type=float, default=300, help="Seconds before the replay.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() events = [(f"e{i}", "2025-10-20", i % 7) for i in range(args.events)] def feed(at): return [(at + i * 0.01, e) for i, e in enumerate(events)] # 1. TTL dedup into an append-only sink seen, sink = {}, [] for t, (eid, day, v) in feed(0) + feed(args.replay_after): if eid in seen and t - seen[eid] < args.ttl: continue seen[eid] = t; sink.append((eid, day, v)) twice = len(sink) - len({e for e, _, _ in sink}) # 2. Replace the partition for the date partitions = {} for at in (0, args.replay_after): batch = [e for _, e in feed(at)] partitions["2025-10-20"] = sorted(batch) # overwrite, not append same = len(partitions["2025-10-20"]) == args.events print(f"TTL dedup ({args.ttl:g} s), replay after {args.replay_after:g} s: {twice:,} of {args.events:,} events applied twice") print(f"overwrite the date's partition: {len(partitions['2025-10-20']):,} rows after the replay (identical: {same})") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"sinks": [{"sink": "append + TTL dedup", "rows_after_replay": len(sink), "applied_twice": twice}, {"sink": "overwrite the partition", "rows_after_replay": len(partitions['2025-10-20']), "applied_twice": 0}]}}, 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 dedup-window-versus-replay.py uv run dedup-window-versus-replay.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --events1000 | Events. |
| --ttl60 | Dedup TTL in seconds. |
| --replay-after300 | Seconds before the replay. |
Our run
A replay five minutes later
| Sink | Rows after the replay | Applied twice |
|---|---|---|
| append + TTL dedup | 2,000 | 1,000 |
| overwrite the partition | 1,000 | 0 |
TTL dedup (60 s), replay after 300 s: 1,000 of 1,000 events applied twice
overwrite the date's partition: 1,000 rows after the replay (identical: True)What we found
The TTL dedup applied 1,000 of the 1,000 events a second time, ending with 2,000 rows. Overwriting the date's partition ended with 1,000 rows, the same as before the replay, as expected.
Read it
This is why a dedup window is a correctness parameter, and why replay needs a different mechanism.
Your dedup store keeps 24 hours and you are replaying six weeks: online dedup cannot help. Design the sink for replay up front: overwrite a partition, swap a table atomically, or apply records only if their version is newer than what is stored.