Idempotent pipelines: why re-running should be boring

A lone figure silhouetted against tall windows in a concrete building at night
Moody editorial photograph of a silhouetted figure against city windows at night
A pipeline you can re-run without flinching is a pipeline you actually operate.

Most explanations of pipeline reliability start with orchestration diagrams. Start somewhere simpler. Ask one question about the job you’re responsible for: if it runs twice on the same input, do you get the same result, or do you get a mess? The answer is the difference between a pipeline you can operate calmly and one that owns you.

What idempotency is

An operation is idempotent if applying it more than once has the same effect as applying it once. Setting a light switch to “off” is idempotent: flip it off twice, the room is just as dark. Nudging a dimmer down one notch is not, because doing it twice leaves you somewhere you didn’t intend. For a data pipeline, idempotency means: run the same job for the same time window as many times as you like, and the output lands in exactly one correct state.

Why it exists, the problem it solves

Pipelines fail partway through. The network drops, the cluster is reclaimed, someone deploys mid-run, an upstream file arrives late. When that happens, you have to run the job again. The whole question is what “again” does. In an idempotent pipeline, a re-run is a non-event: it overwrites its own partial work and converges on the right answer. In a non-idempotent one, the re-run adds to what the failed run already did, and now yesterday’s revenue is counted one-and-a-half times. The failure was free. The retry created the incident.

This is why idempotency isn’t a nicety. It’s the property that lets you retry without fear, and retrying without fear is most of what operating data infrastructure actually is.

The distinctions that trip people up

Append is not idempotent; overwrite is. The most common way pipelines get this wrong is inserting rows. Insert “Tuesday’s events” and fail halfway, retry, and you’ve inserted half of Tuesday twice. The fix is to make each run own a well-defined slice of the output and fully replace it. Delete-then-write, or write to a fresh partition and swap it in atomically. The mental model is a switch, not a dimmer: the run sets the state of its partition, it doesn’t add to it.

Idempotency is defined per key, not per pipeline. “The same input” needs a precise meaning, and that meaning is usually a partition: a day, an hour, a customer. The unit you can safely recompute and overwrite as a whole is the unit your idempotency is built around. Choose it deliberately; it’s the seam the whole design hangs on.

Determinism is a separate promise. A job can be idempotent about where it writes while still producing different values each run, because it read now(), or sampled without a fixed seed, or depended on the order rows happened to arrive. Overwriting a partition with a different answer each time is idempotent and still wrong. For a re-run to be truly boring, the computation has to be reproducible too: pin the clock to the window being processed, fix your seeds, don’t let row order change the result.

At-least-once delivery is the norm, so consumers must absorb duplicates. Most messaging and ingestion systems promise to deliver each record at least once, which means sometimes more than once. You don’t get to opt out of duplicates upstream; you design to be indifferent to them. The usual tools are a natural or synthetic unique key plus an upsert (insert-or-replace) so a repeated record lands in the same slot instead of a new row. “Exactly once” as a marketing phrase almost always means “at-least-once delivery plus idempotent handling on your end.” The second half is your job.

What good looks like

A well-built pipeline can be re-run for any window, any number of times, and land in one correct state: no duplicated rows, no drifting totals, no manual cleanup. Each run claims a partition, computes it reproducibly, and replaces it atomically. When it fails at 3 a.m., the on-call response is to run it again and go back to sleep. The failure was routine because the design made the retry safe.

Where to go deeper

Once the core idea is solid, the natural next topics are atomic partition swaps in your storage layer, upsert and merge semantics in your warehouse, and how backfills, meaning re-running history after a logic change, lean on exactly the same property. But the foundation is this one habit: before you ship a pipeline, run it twice on the same input and confirm the second run changed nothing. A pipeline you can re-run without flinching is worth more than one that’s slightly faster and that everyone is afraid to touch.

Leave a Reply

Discover more from Nullhaus

Subscribe now to keep reading and get access to the full archive.

Continue reading