MLOps
Idempotency, Caching, Backfills, and Recovery
Engineer pipelines that can rerun, resume, backfill, and reconcile state without corrupting data or duplicating irreversible actions.
By the end you can
- Define idempotency for artifact production and external side effects
- Design cache keys that include every behavior-changing dependency
- Plan backfills with bounded scope, reconciliation, and downstream protection
- Choose between retry, resume, compensate, rebuild, and rollback after failure
Recovery begins before the failure
A monthly feature backfill is rerun after a partial outage. The repaired job duplicates rows in one table, recomputes labels using current policy, and triggers thousands of downstream alerts.
The problem is not that the pipeline failed. The problem is that nobody had defined what running the same job twice should mean. Recovery has to be designed as part of the workflow contract.
That question has already been answered, expensively and in public, by people who wrote down what happened. A High Court judgment answered it. So did an order of the U.S. Securities and Exchange Commission, two published postmortems, and the Code of Federal Regulations. The rest of this lesson is built on those documents.
Example
A cache returns yesterday’s policy
A training task reuses a cached dataset because code and source tables are unchanged.
The web hit this failure first and wrote the fix into a standard. RFC 9111, the HTTP caching standard published in 2022, says what a cache is allowed to match on: “The "cache key" is the information a cache uses to choose a response and is composed from, at a minimum, the request method and target URI used to retrieve the stored response.” The load-bearing words are *at a minimum*.
A second rule adds a secondary key. A cache MUST NOT reuse a stored response carrying a Vary header field without revalidation unless every presented request header field nominated by that Vary value matches the request that caused the response to be stored. MDN Web Docs describes the same mechanism in its own words, saying that Vary “causes the cache to be keyed based on a composite of the response URL and the Accept-Language request header — rather than being based just on the response URL”.
A pipeline fingerprint that stops at code and source tables is a primary key with no secondary key at all.
- Hidden dependency: A remote exclusion policy changed after a legal review. No code moved and no source table moved, so nothing the fingerprint watches moved either.
- Incomplete cache key: The policy version was not part of the fingerprint. RFC 9111 treats method and target URI as a floor rather than a definition, precisely so that a second key can carry the fields that change the answer.
- False hit: The orchestrator reuses an artifact built under the previous policy. For HTTP this is the move the standard forbids outright — a stored response whose nominated request fields do not match MUST NOT be reused without revalidation.
- Downstream impact: Evaluation and approval appear internally consistent but apply to the wrong population. Every number agrees with every other number, because they were all computed against the same stale key.
- Repair: Include policy identity and effective time in the cache and release manifest — a secondary cache key by another name. RFC 9111 also supplies the escape hatch for dependencies you cannot enumerate: a stored response whose Vary value contains "*" always fails to match. What you cannot key on, you must not reuse.
Comparison
Backfill and replay have different obligations
Both process historical events, but the intended semantics may differ.
Historical replay reproduces what the system would have known and done at the original time. It runs on the historical code and policy, preserves point-in-time inputs, and supports incident reconstruction. It may deliberately reproduce an old bug — that is the point of it. A corrective backfill recomputes the same history under a declared new rule. It produces a new dataset version, requires downstream impact review, and must never masquerade as original knowledge. Operational reconciliation touches neither past. It compares intended with actual external state and repairs the difference, closing duplicate tickets and restoring one canonical record instead of repeating the work blindly.
Choosing between the three is a claim about which past you are representing. Write the claim down before the run. Nothing in the output distinguishes them afterwards.
Historical replay
Reproduce what the system would have known and done at the original time.
- Uses historical code and policy
- Preserves point-in-time inputs
- Supports incident reconstruction
- May intentionally reproduce an old bug
Corrective backfill
Recompute history under a corrected definition.
- Uses a declared new rule
- Produces a new dataset version
- Requires downstream impact review
- Must not masquerade as original knowledge
Operational reconciliation
Repair current external state after partial execution.
- Compares intended and actual state
- Avoids blind repetition
- May require compensation
- Example: close duplicate tickets and restore one canonical record
Idempotency is scoped to an operation and key
An operation is idempotent when repeating it with the same identity produces the same externally visible result. The identity may include entity, time window, code version, source snapshot, and policy version.
A job can be idempotent for artifact creation but not for email, payment, or ticket creation. Those effects need deduplication, transactional boundaries, or compensating actions.
HTTP wrote the definition down first, and it is worth borrowing. RFC 9110, published in 2022, calls a method idempotent “if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request”. Of the methods it defines, PUT, DELETE and the safe methods qualify. The spec is careful about scope. A server may still log each request, and that logging is a side effect the guarantee never covered.
What a misjudged scope costs is on the court record. Horizon Online repeated the last part of the Pouch Delivery script when the Post Log On script had not closed down. A subpostmaster at Dalmellington scanned £8,000 from her core branch into her outreach branch. The transfer replicated four times. Her receipts showed £32,000, and her branch a discrepancy of £24,000. The High Court set all of this out in Bates v Post Office in 2019. The scan was idempotent as an act of data entry and ruinous as an accounting side effect, and the shortfall landed on the operator rather than on the software.
Nor was it a one-off. The judgment records what the supplier found when it looked: “When investigating this Fujitsu found 112 occurrences affecting 88 different branches in the previous 5 years.” The court counted 65 incidents between February 2010 and January 2011, then 6, 9, 7, 9 and 16 across 2011 to 2015. Fixes were applied in April 2010, January 2011 and January 2016. The duplicated side effect outlived the first two of them. That is what an unspecified repeat looks like when the repeated thing is money.
Visual
Five recovery moves
Different failures call for different operational responses. Retry, resume, reconcile, compensate, and rebuild-or-rollback are not a ladder to climb in order. Each one assumes something the others do not.
The last assumes the most: that trusted inputs still exist. GitLab found out on 31 January 2017 what happens when that assumption has never been tested. An engineer wiped the wrong data directory. The Register reported 300GB of live production data deleted, and that “Just 4.5GB remained by the time he canceled the rm -rf command”. Then the recovery paths were tried one at a time. The pg_dump job had silently been running pg_dump 9.2 against a PostgreSQL 9.6 cluster, so it produced nothing and the S3 bucket was empty. The cron error mail that would have reported this was being rejected by DMARC. GitLab's own postmortem is blunt about the result: “This means we were never aware of the backups failing, until it was too late.”
What restored the site was an LVM snapshot. An engineer had taken it by hand roughly six hours earlier, for load testing. It was not a disaster-recovery mechanism. It was an accident. “The last potentially viable backup was taken six hours beforehand”, The Register noted. Data written between 17:20 and 00:00 UTC was lost: roughly 5,000 projects, 5,000 comments and 700 new user accounts. Four of the five ways out were untested or misconfigured. The fifth was luck.
Retry
Repeat the same operation when it is safe and likely to succeed.
Resume
Continue from a validated checkpoint without repeating completed work.
Reconcile
Compare intended and actual external state, then repair the difference.
Compensate
Apply a new action that reverses or offsets an earlier side effect.
Rebuild or rollback
Recreate from trusted inputs or return to a known release.
Key idea
Backfills can rewrite evidence
Correcting historical data is valuable, but overwriting old snapshots can erase what a model actually saw. Incident analysis then evaluates the past. It uses evidence nobody had at the time.
Preserve immutable historical releases and create a new corrected version. Name whether a downstream consumer needs original-time evidence or corrected analytical truth.
For US broker-dealers this is not advice but law, and the law states it as exactly this choice. An electronic recordkeeping system must either preserve each record with “a complete time-stamped audit trail” — covering all modifications and deletions, the date and time of each action, the identity of the person responsible, and whatever else “will permit re-creation of the original record if it is modified or deleted” — or else “Preserve the records exclusively in a non-rewriteable, non-erasable format;”. That is 17 CFR 240.17a-4(f)(2)(i). Mutability is permitted only where the original stays recoverable.
FINRA dates the audit-trail alternative to the previously mandatory WORM Requirement to the day: “Effective Date: January 3, 2023. Compliance Date: May 3, 2023.” A regulator arrived at the same rule this callout does, and made it enforceable.
Corrected history and historically available information are both useful, but they are not interchangeable.
Steps
Plan a safe backfill
Treat a backfill as a release with its own blast radius. The two steps that fail hardest are the second and the fifth: how much history you meant to touch, and what stops the run.
Bounding the scope is often a single boolean. In the Apache Airflow 3.3.1 documentation, a DAG with a start_date and a non-asset schedule defines a series of data intervals, and “If you set catchup=True in the Dag, the scheduler will kick off a Dag Run for any data interval that has not been run since the last data interval (or has been cleared).” The shipped default is the opposite. The Airflow config scheduler.catchup_by_default is False, so only the latest interval runs. Astronomer's documentation states the same default — “By default, catchup is set to False” — and warns that a DAG running every 5 minutes with a start date of one year ago and catchup set to True will schedule numerous DAG runs at once. One flag is the difference between a bounded scope and every interval since the start date.
The stop condition in step 5 is what stands between a partial deployment and an unbounded one. During a staged deployment beginning 27 July 2012, a technician did not copy Knight Capital's new RLP code to one of the eight SMARS servers. Dormant "Power Peg" code was left running on it, and its cumulative-quantity counter had been moved back in 2005. The SEC's 2013 order against Knight Capital Americas describes what that server then did: “Because the cumulative quantity function had been moved, this server continuously sent child orders, in rapid sequence, for each incoming parent order without regard to the number of share executions Knight had already received from trading centers.”
On 1 August 2012, 212 incoming parent orders reached that eighth server. They produced millions of child orders: 4 million executions in 154 stocks, more than 397 million shares in approximately 45 minutes, an approximately $3.5 billion net long and $3.15 billion net short position, and a $460 million realised loss. Knight paid a $12,000,000 civil money penalty. In October 2012 the Federal Reserve Bank of Chicago published the controls that would have bounded it, among them order-rate limits and a kill switch. Two hundred and twelve orders, no stop condition.
1. State the semantics
Choose replay, correction, or reconciliation and name the target version.
2. Bound the scope
Select entities, partitions, time windows, and downstream consumers.
3. Freeze dependencies
Record code, source snapshots, policy, schema, and effective time.
4. Stage and compare
Run on a sample, inspect diffs, and estimate side effects and cost.
5. Execute with recovery
Use checkpoints, idempotency keys, monitoring, and a stop condition.
A rerunnable pipeline has an explicit memory
The workflow must know what operation it is repeating, which inputs define sameness, and what external state already exists. Without that memory, retries become guesses.
Test recovery by interrupting work deliberately. Waiting for a production outage to reveal the semantics is not a test.
On 28 February 2017 Amazon Web Services learned which of those two it had been doing. By its own account, an authorised team member following an established playbook ran a capacity-removal command in the US-EAST-1 region. One input was entered incorrectly and a larger set of servers was removed than intended. That took down the index subsystem — which holds the metadata and location of all S3 objects in the region — and the placement subsystem. Both required a full restart, and AWS explained why that took as long as it did: “While this is an operation that we have relied on to maintain our systems since the launch of S3, we have not completely restarted the index subsystem or the placement subsystem in our larger regions for many years.”
The index subsystem began serving GET, LIST and DELETE at 12:26 PM PST and was fully recovered at 1:18 PM PST. The placement subsystem finished recovering at 1:54 PM PST. The recovery path existed and was designed for. It had simply never been exercised at the scale it now had to run, so the restart itself became the outage. NPR filed the story as Amazon And The $150 Million Typo, with the cost estimate attributed to Wall Street Journal reporting. The typo is the least interesting part of it.
Defaults decide how often a step repeats, whether or not anyone chose them. In the Kubernetes v1.36 documentation a Job's .spec.backoffLimit “is set by default to 6”, and failed Pods are recreated “with an exponential back-off delay (10s, 20s, 40s ...) capped at six minutes”. Red Hat's OpenShift documentation says the same property “defaults to six” and repeats the same cap. Nobody chose that number for your workload. Six attempts at a step that is not idempotent are six chances to duplicate it.
Key takeaways
- Idempotency is defined for a particular operation identity and external effect: at Dalmellington a single £8,000 transfer replicated four times, producing receipts of £32,000 and a £24,000 discrepancy.
- Cache keys must include every dependency that can change behavior or scope. RFC 9111 makes method and target URI a minimum, then requires a secondary key over every request field the response varies on.
- Historical replay, corrective backfill, and reconciliation serve different goals. A scheduler flag such as Airflow's catchup=True quietly picks one of them for every data interval since the start date.
- Immutable original releases preserve what the system knew at the time. Under 17 CFR 240.17a-4(f)(2)(i), editable records are permitted only alongside an audit trail that will permit re-creation of the original; otherwise the format must be non-rewriteable and non-erasable.
- Recovery may require resume, reconcile, compensate, rebuild, or rollback, not only retry. With no stop condition, 212 parent orders became 4 million executions and a $460 million loss in approximately 45 minutes.
- Failure injection is the most honest test of a pipeline's recovery semantics. GitLab survived on an LVM snapshot taken by accident six hours earlier, and AWS had not completely restarted its S3 index or placement subsystems in its larger regions for many years.