Library// definition

Idempotent ingestion: running the same file twice must change nothing

In short

An ingestion job is idempotent when processing the same input twice leaves the store exactly as processing it once did. The hard part is not the write, it is the key: every record needs an identifier that survives a re-export, a rename and a re-scan, and most of the obvious candidates survive none of the three.

Key takeaways

  • Test it, do not assume it: run the job twice on one input and compare counts and a checksum.
  • File path, arrival order and generated identifiers all fail as keys on the second run.
  • Derived rows need it too. Re-chunking a document leaves orphans unless the old set is removed.
  • Delete-then-insert is idempotent in outcome and leaves a window where the record is missing.
  • Without the property, replay, backfill and retry are all unsafe, so failures become manual work.

An ingestion job is idempotent when processing the same input twice leaves the store in the same state as processing it once. Run the file 5 times, get the state you would have had after 1. That decides whether a failed run can simply be run again.

It is also a property you can test in an afternoon rather than argue about. Ingest a sample, record the row count, the distinct-key count and a SHA-256 checksum over the stored content, then ingest the identical input again and compare all 3. Anything that moved is a defect.

Three keys that look stable and are not

Idempotence is normally implemented as an upsert, and an upsert is only as good as the key it matches on. Three candidates get chosen constantly because they are always available, and each fails on a different ordinary event.

KeyWhy it is chosenThe ordinary event that breaks it
File path or nameAlways present, unique within a folder, needs no source cooperationA rename, a folder move or an export written to a dated directory — the document arrives as a new record
Arrival order or row numberTrivially available in a delimited export, stable within one fileA re-export sorted differently, or one inserted record, shifts every position after it
An identifier generated at ingestionGuaranteed unique, no thought required, no collisionsThe second run mints new identifiers for the same records, so every replay duplicates the corpus
Why the 3 most convenient record keys break, and what breaks them

A fourth candidate deserves a warning because it looks sophisticated. A hash of extracted text is stable against renames and re-exports and unstable against your own code: upgrade the extractor or fix a UTF-8 bug and every key changes at once. Hashing raw bytes avoids that, but many export tools stamp a generation time into the file, so byte-identical content is never delivered twice.

Building a key when the source will not give you one

The order of preference is short, and worth working through in order.

  1. A source-issued identifier, if one exists. A record id, a document number, a claim or invoice reference. It is stable because the source treats it as identity.
  2. A composite of stable content coordinates. Issuer plus document number plus period, or account plus statement date. Write the rule down: the composite must be derivable from every future export.
  3. A hash of the raw bytes, accepting its limits. Usable where the source really does emit identical files and a rewrite of an unchanged document producing a new key is tolerable.
  4. A resolved identity, if nothing above works. You are then matching records rather than keying them — see what record linkage can and cannot decide first.

What the property is worth on the day something fails

Idempotence is invisible while everything works, which is why it gets skipped. Its value appears in four situations, and in each the alternative is manual work under time pressure.

  • Replay after a partial failure. A run that died two-thirds through can simply be run again. Without it, somebody works out which of 30,000 records were written and builds a partial input.
  • Reprocessing history. Every kind of backfill assumes it, because a backfill overlaps existing records by design — the operations separated in the four jobs the word backfill hides.
  • Running a rebuild beside the live pipeline. A periodic full pass is the only way to catch drift from incremental updates, and it is safe only if rewriting an existing record is harmless — rebuilding the whole index against updating only what changed.
  • Retries at the message level. At-least-once delivery is the norm, so a consumer sees some messages twice whatever you do.

Two limits are worth stating plainly. Idempotence guarantees one input processed twice is harmless; it says nothing about 2 sources describing the same entity, which is the precedence question in the golden record in a messy source set. And delete-then-insert satisfies the property while leaving a window where the record is absent — a user asking then gets nothing rather than something slightly old.

It is a good question to ask whoever builds the pipeline: the property is invisible in a demonstration and expensive to retrofit once a corpus is loaded. "Show me the store before and after the same file is ingested twice" takes 10 minutes and separates a working pipeline from one nobody has run twice — worth including in commissioning a build team against hiring in house and in any product build. Both sit in data readiness and pipelines, part of the engineering library.

Frequently asked questions

Short answers to the follow-ups this page tends to raise.

What does idempotent mean in a data pipeline?

It means processing the same input twice leaves the store in the same state as processing it once, so a run that dies halfway can be restarted without anyone working out what it already wrote. Test it by ingesting a sample, recording the row count, distinct-key count and a checksum of the stored content, ingesting the identical input again, and confirming all 3 are unchanged.

Why does reprocessing the same file create duplicate records?

Almost always because the key is not stable across runs. Keyed on the path, an export written to a new dated folder is a whole new corpus; keyed on row position, one inserted line shifts every record after it; keyed on an identifier generated at ingestion, every run mints new ones. Pick a key the source treats as identity, or build a composite from content that will not move.

Is a hash of the document a good ingestion key?

It depends entirely on which bytes you hash, and both options fail somewhere. A hash of extracted text changes the moment you upgrade the extractor or alter a normalisation rule, re-keying the whole corpus. A hash of raw bytes is immune to that but breaks when the source stamps a generation time into each export.

Does idempotence apply to chunks as well as documents?

Yes, and this is where it is most often missed. Re-chunking a document that produced 47 chunks into 43 leaves 4 orphaned rows if the write only upserts by chunk index — still retrievable, quoting text that no longer exists. Delete all children of the document and write the new set in one transaction, or version each chunk set.

  • idempotence
  • ingestion
  • definitions
  • data quality
// shipped work

The work behind this page

Builds from our portfolio that this page draws on.

Working on something in this space?

Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.

Start the conversation