Data Reliability and Fault Tolerance Questions
Designing pipelines that survive failures: retries, idempotency, checkpointing, exactly-once semantics, dead-letter handling, and recovery/replay. Covers reasoning about partial failures, poison messages, and consistency guarantees under faults. The resilience angle distinct from monitoring (detecting and alerting on a failure) and from Workflow Orchestration and Scheduling (the DAG/scheduler mechanics that decide whether and when a task runs again, including backfills and dependency management): this topic owns whether the data itself stays correct, not lost, not duplicated, not corrupted, when a process is retried or replayed.
What is the circuit breaker pattern and how is it used to make downstream API calls safer in data pipelines? Describe parameters such as failure threshold, cooldown window, and how this interacts with retry/backoff policies and backpressure.
Sample Answer
Direct answer
A circuit breaker wraps a downstream call (an API request from a data pipeline) with a state machine that tracks that dependency's recent health and stops calling it entirely once it looks broken, rather than letting every caller keep retrying into a known-failing dependency. It has three states: CLOSED (normal, calls pass through), OPEN (the dependency is considered failing, calls are rejected immediately without even attempting the network call), and HALF-OPEN (a cooldown has elapsed, a small number of probe calls are allowed through to test recovery). This protects both the caller (no more time wasted waiting on doomed calls) and the struggling dependency (no continued load from callers who cannot succeed anyway).
Structured elaboration
Failure threshold. The circuit opens once a configured fraction of recent calls fail (e.g., more than 50% of the last 20 calls, or more than N consecutive failures), not on the first single failure, since a single transient blip should not trip a breaker meant to catch SUSTAINED trouble; the exact threshold trades false-positive risk (opening on normal, isolated hiccups) against false-negative risk (staying closed too long into a genuine outage, still sending traffic that will fail).
Cooldown window. Once open, the circuit stays open for a fixed cooldown period before allowing any probe calls, giving the struggling dependency time to recover WITHOUT continued load from this caller during that window. Too short a cooldown re-opens the circuit into a still-broken dependency repeatedly (thrashing); too long delays recovery detection once the dependency IS actually healthy again.
Interaction with retry/backoff. The circuit breaker and retry-with-backoff operate at different granularities and are complementary, not redundant: backoff governs how AGGRESSIVELY a single caller retries an individual failed call, while the circuit breaker governs whether to attempt the call AT ALL, given the dependency's recent aggregate health. A well-designed system checks the circuit breaker state FIRST (fail fast if open, skip backoff entirely) and only applies backoff-and-retry logic for calls that proceed because the circuit is closed or half-open.
Interaction with backpressure. When the circuit is open, calls that would have gone to the failing dependency are rejected immediately rather than queued indefinitely; this is itself a form of backpressure, signaling upstream (the pipeline stage feeding this call) to either buffer (write to a durable queue), drop, or reroute, rather than accumulating unbounded in-flight work waiting on a dependency that will not respond in time anyway.
Worked example
A pipeline calls a downstream enrichment API at 2,000 requests/sec. The dependency begins failing at 80% of requests (a partial but severe degradation). With a threshold of "open if more than 50% of the last 20 calls failed": within roughly 20/2,000=0.01 seconds of the degradation beginning (the time to accumulate 20 calls at this rate), the failure ratio crosses 50%, and the circuit opens. From that point, roughly:
2,000 requests/sec×cooldown durationworth of requests per second of cooldown are rejected immediately (fast, cheap rejections) instead of each attempting a doomed network call and waiting for its own timeout; at a 30-second cooldown, this is 60,000 requests that would otherwise have each paid a network round-trip's worth of latency waiting to fail, now failing in microseconds instead. After the cooldown, a small number of half-open probe calls (not all 2,000/sec resuming at once) test whether the dependency has recovered; if they succeed, the circuit closes and full traffic resumes; if they still fail, the circuit reopens for another cooldown period.
Trade-offs and pitfalls
- Common mistake: opening on the first failure. This makes the breaker indistinguishable from "stop on any error," far too aggressive for a dependency that has occasional, normal transient blips; the threshold should reflect SUSTAINED degradation, not any single failure.
- Common mistake: resuming full traffic immediately after cooldown instead of a small half-open probe. Sending all 2,000 requests/sec back at once the instant cooldown ends risks immediately re-triggering the same overload/failure condition if the dependency has not FULLY recovered, undoing the cooldown's benefit in the first probe cycle.
- A circuit breaker without any coordination with backpressure just moves the problem, not solves it, per the worked example: rejected calls still need somewhere to go (buffer, drop, or reroute), a design decision separate from the circuit breaker itself.
- Per-dependency circuit state, not a single global breaker, is essential once a pipeline calls MULTIPLE downstream dependencies; a struggling dependency should not trip a breaker that also blocks calls to healthy, unrelated dependencies.
Compare coordinator-based two-phase commit (2PC), a write-ahead-log-plus-idempotent-sink (log-based/replay-and-compaction) approach, and eventual-consistency-with-compensating-transactions for writing to multiple heterogeneous sinks atomically. Discuss failure modes, blocking behavior, performance implications, recovery procedures, hybrid approaches that combine two of these, and cases where none of them is sufficient on its own.
Sample Answer
Direct answer
Two-phase commit (2PC) buys strict atomicity across heterogeneous sinks at the cost of blocking (a coordinator failure mid-protocol can leave participants locked, waiting) and requiring every participant to speak the protocol, which most external systems (a metrics store, a SaaS API) simply do not. A write-ahead-log-plus-idempotent-sink approach (log the intended write durably first, then apply it to each sink idempotently, retrying on failure) trades strict atomicity for eventual consistency with a bounded, observable lag, and works with any sink that supports SOME idempotent write primitive, which is nearly all of them. Eventual-consistency-with-compensating-transactions accepts that a partial write can become temporarily visible and instead commits to detecting and reversing it (a compensating action) if the overall operation ultimately fails, which is the only option when a sink cannot be made idempotent or transactional at all (an external charge API, a one-way notification). In practice, the log-plus-idempotent-sink approach is the default for internal data-pipeline writes; 2PC is reserved for a small number of tightly coupled, protocol-compatible systems; compensating transactions are the fallback for anything that can only be affected, never atomically coordinated.
Structured elaboration
Two-phase commit (2PC).
- Mechanism: a coordinator asks every participant to prepare (durably record readiness to commit, without yet committing); once all participants confirm prepared, the coordinator tells everyone to commit.
- Failure modes: if the coordinator crashes after some participants are prepared but before sending the commit decision, those participants are blocked, holding locks, unable to unilaterally decide commit or abort (the defining weakness of 2PC, sometimes fixed with a separate consensus-based coordinator, but that adds its own complexity).
- Blocking behavior: participants must hold resources (locks, or reserved capacity) from prepare through the final decision, which can be an unbounded wait if the coordinator is slow or down, directly hurting throughput and latency under any coordinator instability.
- Performance: at least two network round trips (prepare, then commit) per transaction across every participant, and locks held for the duration; this scales poorly with more participants or higher latency between them.
- Recovery: a recovering coordinator must consult a durable transaction log to determine, for every in-doubt transaction, what decision it had made (or ask participants, if they retained their own prepared state) before it can safely resume.
Write-ahead-log-plus-idempotent-sink (log-based/replay-and-compaction).
- Mechanism: durably record the intended write once (a WAL entry, or an outbox table row written in the same local transaction as the triggering business logic), then a separate process applies that write to each downstream sink using an idempotent operation (keyed upsert), retrying on failure until it succeeds.
- Failure modes: a crash between logging and applying just means the apply step retries on restart; the log entry is the source of truth for "what should happen," so no work is lost, only delayed. The corresponding risk is a permanently-poisoned entry (an apply that can never succeed, e.g. a malformed record) which needs explicit dead-letter-queue handling and alerting, not indefinite retry.
- Blocking behavior: none. The triggering write completes as soon as the log entry is durable; downstream application happens asynchronously and does not hold the original transaction open.
- Performance: one durable local write up front, then async, retryable, parallelizable application per sink; scales far better than 2PC because sinks are not coordinated in lockstep.
- Recovery: replay unapplied (or possibly-unconfirmed) log entries against each sink; because the sink write is idempotent, replay is always safe even if it turns out the write had actually already succeeded before the crash.
Eventual-consistency-with-compensating-transactions.
- Mechanism: proceed with each sink's write independently (no coordination barrier), and if the overall multi-sink operation later needs to be considered failed, issue an explicit compensating action (a reversing write, a refund, a cancellation notice) against whichever sinks already succeeded.
- Failure modes: a window exists where a partial, eventually-reversed state is visible to any observer reading between the original write and the compensation; if the compensating action itself fails or is not itself idempotent, the system can end up in a state that is neither the original nor fully compensated.
- Blocking behavior: none; this is the most decoupled of the three.
- Performance: best throughput and latency of the three, since nothing waits on cross-sink coordination.
- Recovery: requires tracking which sinks succeeded so compensation knows exactly what to reverse (a saga-style state machine is the standard structure for this), and the compensating actions themselves need the same idempotency discipline as the original writes, or a retried compensation can over-reverse.
Hybrid approaches. The two are frequently combined: log-plus-idempotent-sink for the sinks that support a native idempotent write (the common case), falling back to compensating transactions only for the specific sink that cannot support one (e.g. a third-party API with no idempotency key support), rather than forcing the entire pipeline onto the weakest sink's capability. A concrete 3-system fan-out (a data lake, an OLAP database, a metrics store from one logical write) is exactly this shape in practice: the data lake and OLAP database both typically support idempotent upserts (log-plus-idempotent-sink), while a metrics store that only supports increment operations may need a compensating decrement if the overall write is later rolled back, since increments are not naturally idempotent.
Cases where none is sufficient alone. When a sink can only be affected via a one-way, non-reversible, non-idempotent operation (a push notification already delivered to a user's phone, an SMS already sent), none of the three "fixes" the write after the fact; the only real mitigation is moving the irreversible action to the LAST step of the workflow, after every other sink has already durably committed, so a failure before that point never triggers the irreversible action at all, and any failure after it is a compensating-notification problem ("we already told you X, please disregard") rather than a false one ("we told you X happened, but it did not").
Worked example
A logical write must land in a data lake (Parquet on S3), an OLAP database (for dashboards), and a metrics counter service (increment-only API, no native idempotency key). Using the hybrid approach: the data lake and OLAP writes use an outbox-plus-idempotent-upsert (log-plus-idempotent-sink), keyed by the same event_id. The metrics increment is NOT naturally idempotent (calling increment(+1) twice adds 2, not 1), so it needs a compensating action: on a confirmed failure of the overall operation after the increment already succeeded, issue an explicit increment(-1) keyed to the same event_id (tracked so the compensation itself only fires once). If 1,000,000 logical writes run through this pipeline with a measured 0.1% overall-operation failure rate (a stated assumption for this example):
Each of those 1,000 needs its own idempotent compensating decrement, tracked by the same event_id so a retried compensation attempt does not decrement twice; without that same idempotency discipline applied to the COMPENSATION itself, the metrics counter would show a further 1,000 (or more, under retries) erroneous decrements layered on top of the original 1,000 erroneous increments, doubling the very error the compensation exists to fix.
Trade-offs and pitfalls
- Common mistake: reaching for 2PC by default because it "sounds strongest." 2PC's blocking failure mode is a real production risk (a stuck coordinator can hold locks across every participant indefinitely), and most external sinks simply do not implement the protocol; it is the right tool only when every participant is internal, protocol-compatible, and the operation genuinely cannot tolerate any temporary inconsistency.
- Common mistake: treating compensating transactions as free. A compensating action must itself be idempotent and durably tracked (which operations were already compensated), or the compensation mechanism becomes its own source of double-application bugs, exactly as shown in the worked example above.
- The log-based approach's weak point is a poisoned entry, not a crash. A crash is self-healing (replay resumes); an entry that can NEVER be successfully applied (a permanent schema mismatch, a sink-side rejection) needs explicit DLQ handling and alerting, or it silently retries forever, consuming capacity without making progress.
- The right answer is workload-specific, and a senior answer says so explicitly. A payments ledger touching only internal, transactional sinks may justify 2PC's cost for its atomicity guarantee; a high-throughput analytics fan-out to a dozen heterogeneous sinks almost never can, and belongs on log-plus-idempotent-sink with compensating transactions reserved for the few sinks that cannot support idempotent writes natively.
What does it mean for a data pipeline operation to be idempotent, and why does it matter for a system that retries failed work or replays events? Describe three concrete patterns for making a sink idempotent: upsert or merge by primary key with a version or timestamp, transactional writes with atomic commit, and content-addressable or object-versioned writes. For each, explain when it applies and its trade-offs in cost, latency, and complexity.
Sample Answer
Direct answer
An operation is idempotent when applying it once, or applying it N times with the same input, leaves the system in the same state as applying it exactly once. This matters for pipelines because retries and event replays are inevitable (network timeouts, consumer restarts, at-least-once delivery), and every one of those retries resends work whose first attempt may already have succeeded silently. Without idempotency the pipeline cannot tell "this is new work" from "this is a duplicate of work already done," so retries silently corrupt state: duplicate rows, double-counted aggregates, or a payment charged twice. Three concrete sink-level patterns make writes idempotent: keyed upsert/merge with a version or timestamp, transactional writes with atomic commit, and content-addressable or object-versioned writes.
Structured elaboration
Why "retry-safe" is not automatically "idempotent." A naive retry (call the same INSERT again) is retry-safe in the sense that it does not crash, but it is not idempotent: it produces a second row. Idempotency requires the sink to recognize a repeated logical operation and either no-op it or converge to the same result. That recognition needs a stable key that identifies the logical operation (a business key, an event ID, or a derived content hash), not just the physical retry.
Pattern 1: Upsert or merge by primary key with a version or timestamp.
- When it applies: row-oriented sinks with a natural primary key and a native conditional-write or
MERGEoperation: relational databases, Cassandra, DynamoDB, or table formats like Delta Lake / Apache Iceberg that supportMERGE INTO. - Mechanism: write
INSERT ... ON CONFLICT (pk) DO UPDATE WHERE incoming.version > existing.version(or timestamp-based equivalent). A retry with the same key and same or older version either no-ops or overwrites with an identical value, so the end state is unchanged. - Trade-offs: cost is low, this is standard database machinery. Latency is low for single-row writes, but rises under batch upserts because concurrent writers to the same key contend on row locks. Complexity is medium: you need a monotonic version or timestamp source (a wall clock alone is unsafe under clock skew; a source-system sequence number or Lamport-style counter is safer), and you must define tie-breaking for equal versions.
Pattern 2: Transactional writes with atomic commit.
- When it applies: when a single logical write must land atomically across multiple partitions, tables, or heterogeneous objects, so a partial write is unacceptable. Examples: multi-row OLTP transactions, or a data-lake write that must atomically add multiple Parquet files plus a manifest update (Delta/Iceberg's commit protocol).
- Mechanism: wrap the write in a transaction (or a two-phase commit / atomic manifest swap) keyed by an idempotency token, so a retried transaction with the same token is rejected or recognized as already-applied by the coordinator before any partial state is visible to readers.
- Trade-offs: cost is higher (a transaction coordinator, locks, or an atomic-rename/manifest layer). Latency is higher because the commit protocol adds round trips and the writer must wait for confirmation before releasing correctness guarantees. Complexity is high: the coordinator itself needs to be crash-recoverable, and you must handle the case where the commit succeeded but the acknowledgment was lost (which is exactly what makes retries dangerous in the first place).
Pattern 3: Content-addressable or object-versioned writes.
- When it applies: immutable, append-only sinks such as object storage (S3, GCS) holding blobs or files, where rewriting in place is undesirable or impossible.
- Mechanism: derive the object's key (or a companion manifest entry) from a hash of its content, or from a caller-supplied idempotency token used as the object version. Writing the same content twice produces the same key, so a retry is a no-op PUT to an already-existing key rather than a second distinct object.
- Trade-offs: cost includes extra storage for a hash index or version metadata and eventual garbage collection of superseded versions. Latency for the write itself is low (a PUT is a PUT), but a pre-write existence check or post-write reconciliation adds overhead. Complexity is medium: hashing must be deterministic (careful with non-deterministic serialization, e.g. dict key ordering or float formatting), and this pattern only naturally handles create/append; in-place updates need a separate mechanism.
Worked example
Take a payment-charge API called from a pipeline step, with no idempotency mechanism. Assume the server successfully commits the charge but the acknowledgment is lost on the wire 2% of the time (a realistic order of magnitude for transient network faults), and the client's retry policy blindly resends on any timeout. Over 1,000,000 transactions:
1,000,000×0.02=20,000 duplicate chargesAt an average charge of $50, that is:
20,000×$50=$1,000,000 in erroneous duplicate chargesAdding an idempotency key (pattern 1 or 2 above: the client generates a UUID per logical charge attempt, the server upserts on that key) converts every one of those 20,000 retries into a no-op that returns the original charge's result. The duplicate-charge count drops to (in principle) zero, bounded only by the idempotency key's own storage durability, not by network reliability. The same arithmetic applies directly to duplicate charges and double-counted metrics as the concrete production symptom of a missing idempotency key: a metrics pipeline with the same 2% duplicate-delivery rate and no dedup key would silently inflate every downstream count (revenue, event volume) by roughly 2%, which is large enough to distort a dashboard but subtle enough that nobody notices until finance or product asks why the numbers do not reconcile. The same failure shows up in ETL form too: an ingestion job that retries a failed micro-batch without an idempotency key re-appends the successfully-written rows from the failed batch's completed prefix, corrupting downstream aggregates the same way.
Trade-offs and pitfalls
- Combining patterns is normal, not a code smell. A common production shape is pattern 2 (transactional commit) for the manifest plus pattern 3 (content-addressed files) for the underlying data: the files are naturally idempotent to re-upload, and the transactional manifest swap makes their visibility atomic.
- Common mistake: assuming idempotency is free once a key exists. The idempotency key itself must be generated deterministically from the logical operation (not from
uuid4()at retry time, which produces a new key on every retry and defeats the whole mechanism) and the key-lookup store must itself survive a crash, or the pipeline just moved the duplication problem one layer down. - Common mistake: unbounded idempotency-key retention. Keeping every key forever grows storage without bound; keeping it too briefly (a short TTL) reopens the duplicate window for retries that arrive late (e.g. after a long consumer pause). The retention window must be sized to the pipeline's actual maximum retry delay, not a default.
- Cost/latency/complexity is a real spectrum, not a checkbox. Pattern 1 is usually the cheapest and should be the default; reach for pattern 2 only when atomicity spans multiple objects, and pattern 3 only for genuinely immutable/append-only data, since forcing an update-heavy workload through content-addressing means constantly writing new versions and garbage-collecting old ones.
That is every published Data Reliability and Fault Tolerance question for Site Reliability Engineer (SRE) so far. Browse the other topics in this category, or practice this one interactively.