Module 5: Failure Modes and Resilience for AI
The new failure modes of an AI component
Overview
When you integrate a new component into a system, the first design question isn't "what does it do when it works?" but "how does it fail?". Because everything else depends on that: how you test it, how you monitor it, what defenses you put around it. And here's the turn that makes an LLM a component different from almost any other you've ever wired in: it fails in ways a classic component doesn't have, and the worst of those ways throws no error. A deterministic function has a short, known catalog of failures: either it gives you the correct result, or it throws an exception you catch (ValueError, KeyError, TimeoutError). When it fails, you know: an alarm goes off that you can capture and handle. An LLM inherits all those failures —it can be down, slow, rate-limited, and those do throw an exception— but it adds a new and dangerous one: hallucination, an output that looks perfectly valid, throws no exception, and is wrong. This lesson installs the taxonomy: noisy failures (which an exception catches) and the silent failure (which only a validation sees).
In lesson 1 you saw the module in miniature: a down model and a system that doesn't fall. Here we go down to the question that precedes it: in how many ways does the AI component fail, and how do they differ from those of a normal component? You're going to see, executed, the direct contrast between a classic component —a tax calculation, deterministic— and an AI component —a support-ticket classifier—, counting exactly which types of failure each one produces. The classic one will have zero silent failures; the AI one will have a whole family of them.
Connection with the module. Lesson 1 showed the resilience shell working; this lesson installs the threat map that shell defends —which failure modes exist and which is which—. It's the basis of the whole module: lessons 3 to 7 take each failure mode from this map and develop it with its defense. Lesson 3 takes hallucination (the silent failure); lesson 4 the availability failures (down/slow/rate-limited); lessons 5 and 6 the mitigations (fallback, breaker); lesson 7 drift (a silent failure over time). The boundary with the resilience guide holds: here we catalog the AI-specific failures; the mechanics of the defenses are there.
An analogy: the ATM and the eloquent fortune-teller
Imagine two ways of asking for a figure. The first is an ATM. You ask it for your balance and one of two things happens: either it shows you the correct number, or it gives you a clear error message —"service unavailable", "card not recognized", "try later"—. It never shows you an invented balance. When the ATM can't give you the good datum, it tells you; its failure is noisy, honest, unmistakable. You know exactly when to trust it: when it didn't error.
The second way is asking an eloquent fortune-teller. You ask them for your balance and they always give you a number, said with absolute confidence and a voice that inspires trust. Most of the time they're right —they're good—, but every so often the number is invented, and they say it with exactly the same confidence as when they're right. There's no difference in their tone, in their grammar, in their poise, between the correct answer and the invented one. The fortune-teller never says "I don't know"; they always say something, and that something always sounds equally convincing. Their failure is silent: to know whether they were right, you'd have to verify the number some other way.
Here's the point: a classic component is the ATM; an LLM is the eloquent fortune-teller. The ATM fails noisily —it warns you when it can't—; the LLM can fail silently —it gives you an invented answer with the same face as a correct one—. And this radically changes how you should design around each. You trust the ATM when it didn't error; the error is the signal. You can't trust the fortune-teller by their tone, because the tone is identical whether they're right or not; the only defense is to verify. A system that treats the fortune-teller as if they were an ATM —that trusts their answer because "it didn't error"— will serve invented numbers believing they're real balances. In Mercado, the ticket classifier, the support agent, the description generator: all of them are the eloquent fortune-teller, and the whole module is learning to build the verification system the ATM didn't need.
Worked example: the classic component vs the AI one
We're going to count the failures of each type of component. The classic component is tax_component: it computes a tax. It's deterministic —same input, same output— and fails hard: with an invalid input (a negative subtotal) it throws ValueError. The AI component is ai_component: it classifies a support ticket's topic into one of a closed set of categories (billing, shipping, returns, refund, account). The stub simulates an LLM's own failure modes: sometimes it responds well (ok), sometimes it's down/slow/rate-limited (throws an exception), and sometimes it hallucinates —it returns a category that doesn't exist (teleport, refund9999, premium_tier), with the same naturalness as a valid one, without throwing anything—.
Notice the asymmetry of the AI stub: the down, slow, and rate_limited modes throw an exception; the ok and hallucination modes return a string —and from the outside, without verifying, they're indistinguishable—. That's the whole lesson.
# Lesson 2: the NEW failure modes of an AI component.
# A classic component fails HARD (an exception you catch) or doesn't fail.
# An LLM can fail in new ways, including the SILENT one (hallucination):
# a response that looks correct and is wrong, without throwing any error.
# LLM simulated by a deterministic stub; no network.
# --- CLASSIC component: tax calculation. Deterministic. Fails HARD. ---
def tax_component(subtotal):
if not isinstance(subtotal, (int, float)) or subtotal < 0:
raise ValueError("invalid subtotal") # hard failure, catchable
return round(subtotal * 0.16, 2) # correct if it doesn't throw
# --- AI component: classifies the topic of a support ticket. ---
class ModelDown(Exception): pass
class ModelSlow(Exception): pass
class ModelRateLimited(Exception): pass
# The stub simulates the failure modes NATIVE to an LLM. 'ok' and 'hallucination'
# return text: NEITHER throws an exception. That's the point.
AI_OUTCOMES = [
("ok", "billing"),
("hallucination", "teleport"), # SILENT: invented category
("ok", "shipping"),
("down", None), # the model is down
("ok", "returns"),
("slow", None), # took too long (timeout)
("hallucination", "refund9999"), # SILENT: invented value
("rate_limited", None), # quota exhausted
("ok", "account"),
("ok", "shipping"),
("hallucination", "premium_tier"), # SILENT: category that doesn't exist
("ok", "billing"),
]
VALID_CATEGORIES = {"billing", "shipping", "returns", "refund", "account"}
def ai_component(i):
kind, value = AI_OUTCOMES[i % len(AI_OUTCOMES)]
if kind == "down": raise ModelDown()
if kind == "slow": raise ModelSlow()
if kind == "rate_limited": raise ModelRateLimited()
return value
# --- Classic component: 12 calls, one with invalid input ---
CLASSIC_INPUTS = [100, 250, -5, 0, 1000, 42, 7.5, 300, 15, 999, 1, 50]
classic_ok = classic_err = 0
for x in CLASSIC_INPUTS:
try:
tax_component(x)
classic_ok += 1
except ValueError:
classic_err += 1 # LOUD failure: exception caught
print("=== CLASSIC component (tax calculation) ===")
print(f" successes : {classic_ok}")
print(f" errors (exception) : {classic_err}")
print(f" SILENT failures : 0 <- a deterministic component doesn't have them")
print()
# --- AI component: 12 calls ---
ai_ok = ai_down = ai_slow = ai_rate = ai_silent_wrong = 0
print("=== AI component (ticket classifier) ===")
print(f" {'req':<5}{'mode':<15}{'detected by':<22}result")
print(" " + "-" * 60)
for i in range(12):
try:
out = ai_component(i)
# It didn't throw: it can be 'ok' or a HALLUCINATION.
if out in VALID_CATEGORIES:
ai_ok += 1
print(f" {i:<5}{'ok':<15}{'-':<22}{out}")
else:
ai_silent_wrong += 1 # text that looks valid but is invented
print(f" {i:<5}{'hallucination':<15}{'schema validation':<22}{out} (INVENTED)")
except ModelDown:
ai_down += 1
print(f" {i:<5}{'down':<15}{'exception':<22}fallback")
except ModelSlow:
ai_slow += 1
print(f" {i:<5}{'slow':<15}{'exception (timeout)':<22}fallback")
except ModelRateLimited:
ai_rate += 1
print(f" {i:<5}{'rate_limited':<15}{'exception':<22}fallback")
print(" " + "-" * 60)
print(f" ok={ai_ok} down={ai_down} slow={ai_slow} rate_limited={ai_rate} "
f"hallucinations={ai_silent_wrong}")
print(f" LOUD failures (exception) : {ai_down + ai_slow + ai_rate}")
print(f" SILENT failures (hallucination) : {ai_silent_wrong} "
f"<- they throw NO exception; only a validation sees them")
What to expect. When you run the file, the output is exactly this:
=== CLASSIC component (tax calculation) ===
successes : 11
errors (exception) : 1
SILENT failures : 0 <- a deterministic component doesn't have them
=== AI component (ticket classifier) ===
req mode detected by result
------------------------------------------------------------
0 ok - billing
1 hallucination schema validation teleport (INVENTED)
2 ok - shipping
3 down exception fallback
4 ok - returns
5 slow exception (timeout) fallback
6 hallucination schema validation refund9999 (INVENTED)
7 rate_limited exception fallback
8 ok - account
9 ok - shipping
10 hallucination schema validation premium_tier (INVENTED)
11 ok - billing
------------------------------------------------------------
ok=6 down=1 slow=1 rate_limited=1 hallucinations=3
LOUD failures (exception) : 3
SILENT failures (hallucination) : 3 <- they throw NO exception; only a validation sees them
Read the two sections in contrast, because that's where the whole lesson is.
The classic component has a catalog of results with two entries: 11 successes and 1 error (the input -5, which threw ValueError). And a line that's the heart of the contrast: silent failures: 0. A deterministic component doesn't have that category. When tax_component can't give you a valid result, it knows and says so —it throws an exception—. It never returns you an invented tax that looks correct. Its failure is always noisy, and that's why trusting it is easy: if it didn't throw, it was right.
The AI component has a much richer catalog. Of 12 calls: 6 correct, 3 noisy failures (1 down, 1 slow, 1 rate-limited —each one threw its exception, and the system could fall to the fallback—), and —here's the new thing— 3 hallucinations. Look at requests 1, 6, and 10: the model returned teleport, refund9999, and premium_tier. None threw an exception. All three are strings, like the six good ones. From the outside, a try/except lets them all through equally —the except never fires because there was no exception—. The only reason this code detected them is the line if out in VALID_CATEGORIES: a schema validation that compares the output against the closed set of real categories. Without that validation, teleport would have been used as if it were a real category, with the consequences that drags downstream (a ticket routed to a team that doesn't exist, garbage data in a report).
Notice the architectural implication: the try/except protects against the noisy failures, and does absolutely nothing against the silent ones. The three noisy failures fell to the fallback cleanly. The three hallucinations would have passed as good if there hadn't been an explicit validation of the output's content. They're two distinct defense fronts: one catches exceptions (for the down/slow/rate-limited model), the other validates content (for hallucination). The module covers both, and this lesson shows you why you need both: neither covers the other's turf.
Going deeper: the map of AI failure modes
It's worth laying out the complete map, because it's the module's compass. An AI component's failure modes group into three families, and each lesson takes one:
FAILURE MODES OF AN AI COMPONENT
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
AVAILABILITY CONTENT TEMPORAL
(noisy) (silent) (silent, slow)
│ │ │
┌─────┼─────┐ │ │
▼ ▼ ▼ ▼ ▼
down slow rate- hallucination drift
(timeout) limited (invents with (degradation
confidence) over time)
│ │ │
throws exception throws NOTHING; throws NOTHING;
-> timeout/breaker/ looks valid appears gradually
fallback (L4,L5,L6) -> verify (L3) -> monitor (L7)
Family 1: availability failures (noisy). The model down (the API doesn't respond, HTTP 5xx), slow (it takes longer than your budget, and without a timeout it leaves you waiting), and rate-limited (you exhausted the quota, HTTP 429). All three are noisy: the API returns an error or the timeout turns it into one. They're defended with the classic resilience pieces —timeout (L4), fallback (L5), circuit breaker (L6)— applied to the model. They're the "easy" ones in the sense that you at least know when they happen.
Family 2: content failure (silent). The hallucination: the model responds —with no error— with an invented datum that sounds good. It's AI's own failure, the one that doesn't exist in any classic component. No try/except catches it; only a verification of the content against a source of truth sees it (L3). It's the most dangerous because it's invisible: the system believes everything is going well.
Family 3: temporal failure (silent, slow). Drift: the model or the data change over time and the quality degrades little by little, with no fall or exception. Today the system gets 94% right, in a month 79%, and nobody noticed because there was never a moment of "it broke." Only a monitoring that measures quality over time sees it (L7). It's a hallucination distributed over weeks.
The key distinction: noisy vs silent. A noisy failure announces itself —it throws an exception, returns an error code, exhausts a timeout—; your code can capture it in the moment and react. A silent failure doesn't announce itself —it delivers an output that passes for good—; your code will never capture it unless you actively verify that the output is correct. This is the deep reason the LLM's output is treated as untrusted (module 4's thesis): not because the model is bad, but because you can't distinguish its success from its failure by the form of the response. The fortune-teller sounds the same whether they're right or not.
Why the silent one is the expensive one. A noisy failure, by definition, gives you the chance to handle it: you fall to the fallback, escalate, retry. The damage is bounded —a slower request, a degraded response—. A silent failure reaches the user with no friction, because nothing stopped it. A badly computed tax that threw an exception would be caught; a badly computed tax that was returned as correct would reach the customer's invoice. The classic component never does the latter; the AI one does. That's why the module dedicates a whole lesson (3) to hallucination and another (7) to drift: they're the two silent failures, and against silence the only defense is looking actively.
The noisy failures are also new in their origin, though not in their form. Careful with a simplification: although a down model resembles any other down dependency, its origin has nuances of its own to AI. The rate limit is more central than in a classic service —the model provider imposes strict quotas per tokens and per requests—; slowness is the normal case, not the exception (an LLM takes hundreds of ms to seconds, as you saw in module 2), so the timeout isn't insurance against oddities but a first-class constraint; and the cost per call turns each retry into money. That's why, although the form of defending (timeout, breaker, fallback) is the same as in the resilience guide, the motivation here carries extra weight. Lessons 4 and 6 develop it.
Common mistakes
Treating the LLM as an ATM (trusting that "if it didn't error, it was right"). What happens: the team wraps the model call in a careful try/except, handles the down model and the rate limit well, and takes for granted that this covers it. But it never validates the content of the responses that didn't throw an exception, so all the hallucinations pass as good. Why it happens: the mental model of a classic component is transferred to the LLM, where "it didn't throw = it was right" is true. In the LLM it's false. How to spot it: your model-failure handling consists only of except, with no verification of the content of the successful output. How to fix it: add the second front —a validation of the content against a schema or a source of truth— because the try/except doesn't see the hallucinations. Lesson 3 executes it.
Counting only the noisy failures in the metrics. What happens: the dashboard shows "99.5% of model calls successful" and the team sleeps soundly —because it counts as "successful" every call that didn't throw an exception, including the ones that hallucinated—. The availability rate is excellent and the correctness rate is a disaster, but only the first is measured. Why it happens: availability is easy to measure (did it throw or not?), correctness is hard (you have to verify the output). How to spot it: your model-health metrics don't include any measure of output quality, only of availability. How to fix it: measure both —availability (noisy failures) and quality (silent failures, with module 3's eval)—, because a model can be 100% available and still serve garbage. Lesson 7 takes it to continuous monitoring.
Confusing "rare" with "doesn't happen." What happens: the team sees that hallucinations are 5-10% and decides it's a rate low enough to ignore —"the model almost always gets it right"—. At scale, that 5-10% is thousands of invented responses served a month, each indistinguishable from the good ones. Why it happens: the rate is thought of as the magnitude of the problem, when the problem is that you don't know which are the bad ones without verifying. How to spot it: your justification for not verifying is the failure's low frequency. How to fix it: the frequency decides how much you degrade or retry, not whether you verify; the verification always goes, because a single hallucination served in a critical datum (an order status, an amount) can cost dearly. It's the same logic as module 4's "almost always right is the trap."
Exercises
Exercise 1 — Classify the failure mode. For each situation, say which family it belongs to (availability / content / temporal), whether it's noisy or silent, and what the appropriate defense is: (a) the model's API takes 25 seconds and the request hangs; (b) the model recommends a product that was withdrawn from the catalog a month ago; (c) the semantic search's eval score dropped from 0.91 to 0.82 in six weeks; (d) the API returns HTTP 429.
See solution
- (a) Takes 25 s → AVAILABILITY, noisy (with a timeout). The hang on its own throws nothing, but the timeout turns it into a catchable noisy failure. Defense: timeout (cut at your budget) + fallback. Lesson 4.
- (b) Recommends a withdrawn product → CONTENT, silent. It's not an exception; the model returned an ID that looks valid but points to something that should no longer be shown. It's a form of hallucination/stale datum. Defense: verify the ID against the source of truth (does the product exist and is it active?) before serving it. Lesson 3.
- (c) The eval dropped 0.91 → 0.82 in six weeks → TEMPORAL, silent. Drift: slow degradation with no fall. Defense: continuous monitoring of the eval with an alert. Lesson 7.
- (d) HTTP 429 → AVAILABILITY, noisy. Explicit rate limit. Defense: circuit breaker (stop calling so as not to make it worse) + fallback. Lesson 6.
Exercise 2 — The try/except that isn't enough. A colleague shows this code and says it "already handles all the model's failures":
try:
category = ai_component(ticket)
route_to_team(category) # route the ticket to that category's team
except (ModelDown, ModelSlow, ModelRateLimited):
route_to_team("general") # fallback
Explain which failure family it handles well and which it lets through entirely, and give a concrete example of what can go wrong.
See solution
The code handles the availability family well (noisy failures): if the model is down, slow, or rate-limited, it throws the exception, the except catches it, and the ticket is routed to the general team as a fallback. That's correct.
What it lets through entirely is the content family (the silent failure, the hallucination). When ai_component returns teleport or premium_tier —an invented category—, it throws no exception, so the except never fires, and route_to_team("teleport") runs with a category that doesn't exist. Concrete example of what goes wrong: the ticket is routed to a "teleport" team that doesn't exist, so the ticket is lost —nobody receives it— or the router blows up with an error much deeper, far from the cause. The fix: add a validation of the content before using the category:
try:
category = ai_component(ticket)
if category not in VALID_CATEGORIES: # catches the hallucination
category = "general"
route_to_team(category)
except (ModelDown, ModelSlow, ModelRateLimited):
route_to_team("general")
The try/except covers the noisy failure; the if ... not in VALID_CATEGORIES covers the silent one. You need both.
Exercise 3 — The failure a classic component can't have. Explain, in your own words, why tax_component (the example's classic component) can't have a silent failure equivalent to hallucination, and what property of the LLM makes it able to have one. Then describe a case in Mercado where a hallucination would be especially expensive.
See solution
tax_component can't hallucinate because it's deterministic and closed: given a valid subtotal, there's exactly one correct result (subtotal * 0.16), and the code computes it always. There's no room to "invent" a tax that looks plausible but is wrong; the calculation is the calculation. When the input is invalid (a negative subtotal), it doesn't return a doubtful number: it throws an exception. Its output space is {correct result, exception}. There's no third option "invented result that looks correct."
The LLM can have one because it's probabilistic and open: it doesn't compute the correct answer from a formula; it generates a plausible output token by token from patterns. When it doesn't "know" the answer, it doesn't flag it —there's no internal mechanism that says "I don't know this"—; it simply generates the most plausible continuation, which can be an invented datum said with total fluency. Its output space includes the third option: "output that looks correct and isn't."
An expensive case in Mercado: the support agent that invents an order status or a tracking number. If the agent tells a customer "your order was delivered yesterday" when it's actually lost, or gives them an invented tracking number, the customer acts on a false datum —they stop waiting, don't complain in time, or go looking for a package that didn't arrive—. The cost isn't just the bad answer: it's the chain of decisions the customer makes trusting it. That's why the agent must verify every factual datum against the source of truth before serving it, which is exactly lesson 3.
Summary and next step
In this lesson you installed the module's threat map: an AI component's failure modes group into three families —availability (down/slow/rate-limited, noisy), content (hallucination, silent), and temporal (drift, silent and slow)— and the distinction that organizes them is noisy vs silent. You saw it with the ATM (which warns you when it can't) and the eloquent fortune-teller (who invents with the same face as being right), and you measured it: the classic component had 11 successes, 1 error, and zero silent failures; the AI one had 6 correct, 3 noisy failures the try/except caught, and 3 hallucinations only a content validation could see. The architectural lesson: you need two defense fronts —catch exceptions for the noisy, verify content for the silent—, and neither covers the other's turf.
Before moving on you should be able to: name the three failure families and give an example of each; distinguish a noisy failure from a silent one and explain why the silent is the expensive one; argue why a try/except doesn't defend against a hallucination; and explain what property of the LLM (probabilistic, open) makes the silent failure possible that a deterministic component doesn't have.
Lesson 3 takes the most native and dangerous failure of all —hallucination— and develops it in depth. You're going to see, executed, Mercado's support agent citing order statuses and tracking numbers, and a verification against the source of truth that separates the real from the invented: the data that match the orders' real status are served, and the ones the model invented are blocked before reaching the customer, degrading to a "I can't confirm it, let me connect you with an agent." The way to turn "the model sometimes lies" into a gate that catches the lie.
Resources
- Chip Huyen, AI Engineering (O'Reilly, 2024). The chapters on reliability and evaluation treat hallucination and content failures as a class of failure distinct from availability ones, and why output verification is part of the design. This lesson's central reference for the taxonomy. In English.
- Anthropic, Claude documentation — docs.anthropic.com. The pages on rate limits and API errors describe the availability failure modes of an API-served model (429, 5xx), at a conceptual level and without fixing a version. In English.
resilience-and-reliability-patterns-guide(this ecosystem), M1 "Why distributed systems fail" — the general framing of partial failure and a dependency's failure modes, which here we specialize to the AI component. In Spanish.- Martin Fowler and Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. It places output validation and failure handling around an AI component. In English.