{"slug": "one-bad-step-n-bad-steps-how-agent-failures-cascade", "title": "One bad step, N bad steps: how agent failures cascade", "summary": "A developer's analysis of LLM agent failures reveals that per-step error rates underestimate real-world risk because errors are coupled through the agent's context, causing cascades that amplify a single mistake into multiple failures. Simulations show that a 10% baseline step-error rate can double the expected faults per run when contamination elevates subsequent failure probabilities, highlighting the structural nature of these cascades.", "body_md": "*Originally published on Loop & Retry — field notes on building LLM agents that survive production.*\n\nHere's the failure mode that surprises people who've only reasoned about agents statistically. You measure a per-step error rate — say 10% of steps produce something wrong — and you assume errors are independent, so a wrong step is a wrong step and the rest of the run is fine. Then you watch a real trajectory and see something else: step 4 gets a fact slightly wrong, step 5 reasons *on top of* that wrong fact and commits harder, step 6 takes an action premised on both, and by step 8 the agent is confidently executing a plan that was doomed at step 4. One mistake became five. The errors weren't independent — they were **coupled through the context**, and coupling is what turns a 10% step-error rate into a run that's wrong far more than 10% of the time.\n\nThis is the *cascade*: a single fault amplifying down a single trajectory. It's distinct from the failure I wrote about in [distributed retry patterns](https://loopandretry.github.io/posts/fleet-retry-patterns/?ref=devto), where the problem is one bad condition hitting *many workers at once* — that's a blast radius, a horizontal spread. The cascade is vertical: it spreads through *time* within one run, because an agent's own past output is its future input. This post is about the vertical kind, why it's structural rather than bad luck, and where you can cut it.\n\nA stateless function that fails just returns an error. An agent that fails does something worse: it *writes the failure down where it can read it again.* The mechanism is the same one that makes agents work at all — the transcript accumulates, and every step conditions on everything before it. That's a feature for carrying intent forward. It's also the exact channel a mistake travels down.\n\nThree ways a single fault propagates through the context:\n\nIn all three, the common structure is: the fault becomes part of the state the agent reasons from, so it's no longer one wrong step — it's a wrong *starting condition* for every step that follows.\n\nLet's put a number on it. Model a run as N steps. Each step, absent any prior damage, fails on its own with probability `p`\n\n. But once a run is \"contaminated\" — a fault has entered the context — every subsequent step fails with an elevated probability `p_c > p`\n\n, because it's reasoning on a poisoned premise. That's the coupling, expressed as one conditional.\n\n``` python\nimport random, statistics\n\nN   = 8       # steps per run\np   = 0.10    # baseline per-step fault probability\np_c = 0.45    # per-step fault probability ONCE the run is contaminated\n\ndef run_cascade(trials=200_000):\n    total_faults, contaminated_runs = 0, 0\n    for _ in range(trials):\n        contaminated, faults = False, 0\n        for _step in range(N):\n            fail_prob = p_c if contaminated else p\n            if random.random() < fail_prob:\n                faults += 1\n                contaminated = True          # the fault poisons the rest of the run\n        total_faults += faults\n        contaminated_runs += 1 if contaminated else 0\n    return total_faults / trials, contaminated_runs / trials\n\ndef run_independent(trials=200_000):\n    total = sum(sum(random.random() < p for _ in range(N)) for _ in range(trials))\n    return total / trials\n\nfaults_coupled, contam = run_cascade()\nfaults_indep = run_independent()\nprint(f\"independent model: {faults_indep:.2f} faults/run\")\nprint(f\"coupled model:     {faults_coupled:.2f} faults/run  ({contam*100:.0f}% of runs contaminated)\")\nprint(f\"amplification:     x{faults_coupled/faults_indep:.2f}\")\n```\n\nRunning it:\n\n```\nindependent model: 0.80 faults/run\ncoupled model:     1.61 faults/run  (57% of runs contaminated)\namplification:     x2.01\n```\n\nSame 10% baseline step-error rate. Under the independent assumption you expect 0.8 faults per run and move on. Under coupling you get **twice as many**, and more than half your runs end up contaminated — carrying at least one fault that then bred more. The baseline `p`\n\ndidn't change. What changed is that the model stopped pretending a mistake sits still.\n\nAnd the cascade gets *worse* with run length, which is the tell that distinguishes it from independent noise:\n\n```\nN=4    x1.5 amplification\nN=8    x2.0\nN=16   x2.7\nN=25   x3.2\n```\n\nIndependent faults scale linearly with N — twice the steps, twice the expected faults, same *rate*. Cascading faults scale super-linearly, because a longer run gives an early fault more downstream steps to poison. This is the same shape as the [O(N²) token curve](https://loopandretry.github.io/posts/long-agent-runs-are-quadratic/?ref=devto): long agent runs are where the structural problems live, cost and correctness alike.\n\nYou can't drive `p`\n\nto zero. The leverage isn't in never making the first mistake — it's in stopping the first mistake from becoming the next five. Three interruption points, roughly in order of leverage:\n\n`p_c`\n\n, not just `p`\n\n.`p_c`\n\nfrom 0.45 to 0.22 drops the coupled model from 1.61 faults/run to about 1.08 — most of the way back to the 0.80 independent floor. That's a bigger win than any realistic cut to `p`\n\n.Notice these are different tools than the fleet post prescribed. Circuit breakers and shared budgets bound the *horizontal* spread across workers; they do nothing for the *vertical* spread inside one run. A single worker with a clean circuit breaker can still cascade itself into a completely wrong answer. You need both: blast-radius controls for the fleet, cascade controls for the trajectory. And if you're running [multi-agent crews](https://loopandretry.github.io/posts/multi-agent-failure-modes/?ref=devto), add a third dimension: one agent's cascade can become another agent's poisoned input, turning local failures into crew-level coordination breakdowns.\n\n`p_c`\n\n.`p`\n\npolishes individual steps while leaving the amplification untouched — and the amplification is most of the problem.One bad step is not one bad step. It's a starting condition, and the agent will faithfully build on it until something makes it stop. Your job isn't to prevent the first mistake — it's to make sure the second one doesn't inherit it.\n\n*The cascade model here is a toy Monte Carlo with a single contamination state; real trajectories have partial recovery and varying p_c by step, which you can add. The structural claims — coupling through context, super-linear scaling with length, p_c as the dominant lever — transfer across providers and models.*", "url": "https://wpnews.pro/news/one-bad-step-n-bad-steps-how-agent-failures-cascade", "canonical_source": "https://dev.to/loopandretry/one-bad-step-n-bad-steps-how-agent-failures-cascade-538g", "published_at": "2026-08-11 09:56:22+00:00", "updated_at": "2026-08-11 10:18:05.159114+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "ai-safety"], "entities": ["Loop & Retry"], "alternates": {"html": "https://wpnews.pro/news/one-bad-step-n-bad-steps-how-agent-failures-cascade", "markdown": "https://wpnews.pro/news/one-bad-step-n-bad-steps-how-agent-failures-cascade.md", "text": "https://wpnews.pro/news/one-bad-step-n-bad-steps-how-agent-failures-cascade.txt", "jsonld": "https://wpnews.pro/news/one-bad-step-n-bad-steps-how-agent-failures-cascade.jsonld"}}