Idempotent sink with a conditional insert
Break it on purposeNeeds Python
Problem
At-least-once delivery means the sink will see some records twice, so the sink has to make a repeat harmless. The database can do that if it enforces uniqueness: a primary key on the record id plus "insert, and do nothing on conflict". Doing the same check in application code (select to see whether the row exists, then insert if it does not) looks equivalent and is not: two consumers can both check, both see nothing, and both insert.
Take the output of the previous lab (Auto-commit versus commit-after-process) and write the records into SQLite or PostgreSQL instead of a file. First use a plain INSERT into a table without a key and repeat the crash test: count rows per record number. Then add PRIMARY KEY (record_id) and use INSERT ... ON CONFLICT DO NOTHING. Then break it on purpose: implement dedup as "SELECT, then INSERT if absent" in application code and run two consumer processes on the same partition set.
The script redelivers a 100-record batch after each of 10 simulated crashes, and runs the check-then-insert version as two concurrent consumers with their own connections.
The question. How many duplicates does each sink end with: a plain insert, a keyed conditional insert, and check-then-insert run by two consumers?
Predict
Plain insert gives duplicates after every crash; the conditional insert gives exactly zero duplicates; the select-then-insert version still produces occasional duplicates when two processes interleave.
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
idempotent-sink-with-a-conditional-insert.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Idempotent sink with a conditional insert. Records written to SQLite by a consumer that crashes and redelivers its last batch: a plain INSERT into a table without a key, a PRIMARY KEY with INSERT ... ON CONFLICT DO NOTHING, and dedup done in application code as "SELECT, then INSERT if absent" by two consumers at once. """ import argparse, json, os, random, sqlite3, tempfile, threading, time p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--records", type=int, default=10_000, help="Records.") p.add_argument("--batch", type=int, default=100, help="Records per commit (the duplicate blast radius on a crash).") p.add_argument("--crashes", type=int, default=10, help="Crashes during the run.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() random.seed(args.seed) def consume(db, insert_sql): """Apply records in batches; each crash happens after a batch is written but before its offset is committed, so the batch is delivered again on restart.""" crash_at = set(random.sample(range(args.batch, args.records, args.batch), args.crashes)) pos = 0 while pos < args.records: batch = range(pos, min(pos + args.batch, args.records)) db.executemany(insert_sql, [(r, f"v{r}") for r in batch]); db.commit() if pos in crash_at: crash_at.discard(pos); continue # crash: offset not committed, redeliver pos += args.batch def check_then_insert(path, ids, barrier): db = sqlite3.connect(path, timeout=30, isolation_level=None) barrier.wait() for r in ids: if db.execute("SELECT 1 FROM t WHERE id = ?", (r,)).fetchone() is None: # check ... time.sleep(0.0002) db.execute("INSERT INTO t VALUES (?, ?)", (r, f"v{r}")) # ... then act db.close() rows = [] for name, ddl, sql in (("plain INSERT, no key", "CREATE TABLE t (id INTEGER, v TEXT)", "INSERT INTO t VALUES (?, ?)"), ("PRIMARY KEY + ON CONFLICT DO NOTHING", "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", "INSERT INTO t VALUES (?, ?) ON CONFLICT DO NOTHING")): db = sqlite3.connect(":memory:"); db.execute(ddl) consume(db, sql) total, distinct = db.execute("SELECT COUNT(*), COUNT(DISTINCT id) FROM t").fetchone() rows.append({"sink": name, "rows": total, "duplicates": total - distinct}) path = os.path.join(tempfile.mkdtemp(), "race.db") db = sqlite3.connect(path); db.execute("CREATE TABLE t (id INTEGER, v TEXT)"); db.execute("PRAGMA journal_mode=WAL"); db.commit(); db.close() barrier = threading.Barrier(2) ids = list(range(2000)) ps = [threading.Thread(target=check_then_insert, args=(path, ids, barrier)) for _ in range(2)] # two consumers, own connections [x.start() for x in ps]; [x.join() for x in ps] db = sqlite3.connect(path) total, distinct = db.execute("SELECT COUNT(*), COUNT(DISTINCT id) FROM t").fetchone() rows.append({"sink": "SELECT then INSERT, two consumers", "rows": total, "duplicates": total - distinct}) for r in rows: print(f"{r['sink']:<38} {r['rows']:>6} rows, {r['duplicates']:>5} duplicates") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"sinks": 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 idempotent-sink-with-a-conditional-insert.py uv run idempotent-sink-with-a-conditional-insert.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --records10,000 | Records. |
| --batch100 | Records per commit (the duplicate blast radius on a crash). |
| --crashes10 | Crashes during the run. |
| --seed0 | Random seed. |
Our run
The same redeliveries, three sinks
| Sink | Rows | Duplicates |
|---|---|---|
| plain INSERT, no key | 11K | 1,000 |
| PRIMARY KEY + ON CONFLICT DO NOTHING | 10K | 0 |
| SELECT then INSERT, two consumers | 2,960 | 964 |
plain INSERT, no key 11000 rows, 1000 duplicates
PRIMARY KEY + ON CONFLICT DO NOTHING 10000 rows, 0 duplicates
SELECT then INSERT, two consumers 2964 rows, 964 duplicatesWhat we found
The plain insert ended with 1,000 duplicates, one per redelivered record. The keyed conditional insert ended with 0. Check-then-insert by two consumers ended with 964 duplicates: far more than "occasional", because both consumers worked through the same ids at the same moment, so most of their checks raced.
Read it
This shows that check-then-act is a race and that the database must enforce uniqueness.
For every effect, ask: what is the key and what is the store? If nobody can answer in one sentence, that effect is at-least-once with no dedup, which means it produces duplicates. The unique constraint is the dedup, and it is the only version that holds under concurrency.