p99log

Sep 7, 2026 / Systems, AI, Reliability

Synthetic Resilience

A coding agent can turn a dependency failure into silent, incorrect behavior by inventing a fallback your system was never designed to use.

Call it synthetic resilience: code that appears robust because it keeps running, while quietly replacing a real failure with an invented answer.

A small example

Say your app converts dollars to another currency. You call an exchange-rate API to get the number. That part is fine.

Then you ask an AI agent to add error handling. It does. But it adds more than error handling. It adds a fallback number. If the API is down, the app uses that number instead.

public async Task<decimal> GetUsdToEurRateAsync(
  CancellationToken cancellationToken)
{
  try
  {
    return await exchangeRates.GetRateAsync(
      "USD", "EUR", cancellationToken);
  }
  catch (HttpRequestException)
  {
    return 0.92m;
  }
}

The method still returns a decimal, so every caller sees an ordinary successful result. The type carries no indication that the API failed or that 0.92 has different provenance from a live quote.

Where did that number come from? Not from your business rules. Not from a rate provider. The model produced a plausible completion from the prompt and the code around it. It cannot tell you whether 0.92 resembles an old exchange rate, a placeholder from an example, or simply a likely-looking decimal. It is a guess, dressed up as code.

Now play that forward. The API goes down for ten minutes. Your app keeps working. No error. No alert. It just quietly charges customers using a stale, guessed exchange rate. Nobody notices until someone checks the numbers a week later.

That is worse than the API just failing. A clear failure gets fixed fast. A silent wrong answer gets fixed slowly, after it has already cost you something.

Why the agent does this

The model is not evaluating your business invariant. It is generating a likely continuation from your prompt, the repository context, and patterns learned during training.

Ask it to “add error handling” and a catch block with a default return value is a familiar, complete-looking pattern. It satisfies the local shape of the request: the exception is handled and the method still returns its declared type. Nothing in that local pattern proves the returned value is valid for a financial transaction.

That is the gap. The agent can reproduce a resilience pattern without knowing whether degraded behavior is allowed. Unless the requirement, type, test, or surrounding code encodes that decision, the model has no reliable basis for making it.

The result is code that looks defensive in review while quietly changing the contract of the system. An unavailable exchange rate used to stop a transaction. Now it produces one.

This is not a new problem

I’ve seen this exact shape before, outside of AI code. My last post covered IP security cameras and a standard called ONVIF. A camera can claim to support something and be wrong. The fix was never “trust the label.” The fix was: test it live, decide your fallback on purpose, and make sure the fallback is visible when it fires.

The same three rules apply here.

Three questions to ask about any fallback

Before you accept a fallback an agent wrote - or one you wrote yourself - ask these:

Did we choose this, or did it just show up? A fallback should be a decision. “If the exchange-rate API fails, we show an error and block the transaction” is a decision. A hardcoded number nobody reviewed is not.

What happens when it fires? Some failures should be loud. If money, safety, or a customer promise is on the line, a fallback should log clearly, alert someone, or fail the request outright. Quiet is only okay for things that truly don’t matter.

Would we notice if it ran 100 times today? If your fallback path fired constantly and nobody would know, that’s the real danger. Not that the fallback exists. That it can run forever with nobody watching.

A legitimate fallback makes those decisions explicit. For example, the business might approve a cached rate that is no more than five minutes old. That policy has a source, a limit, and an observable path:

public async Task<decimal> GetUsdToEurRateAsync(
  CancellationToken cancellationToken)
{
  try
  {
    return await exchangeRates.GetRateAsync(
      "USD", "EUR", cancellationToken);
  }
  catch (HttpRequestException exception)
  {
    var cached = await rateCache.GetAsync(
      "USD", "EUR", cancellationToken);

    if (cached is not null &&
      clock.UtcNow - cached.RetrievedAt <= TimeSpan.FromMinutes(5))
    {
      logger.LogWarning(exception,
        "Using cached USD/EUR rate from {RetrievedAt}",
        cached.RetrievedAt);
      fallbackCounter.Add(1);
      return cached.Value;
    }

    throw new ExchangeRateUnavailableException(
      "No current USD/EUR rate is available.", exception);
  }
}

This fallback is not safe merely because it uses a cache. It is safer because someone chose the cache as an approved source, bounded its staleness, made its use visible, and defined when the transaction must fail.

What to do about it

You don’t need to ban AI-written code. You need one habit: read every fallback like you wrote it yourself, because soon it will be your bug.

When you see a fallback in an AI-generated diff, stop and ask:

If you can’t answer those three questions, don’t merge it yet.

The bigger point

Fallbacks aren’t bad. Bad fallbacks are bad: the ones nobody chose, nobody logs, and nobody remembers exist until they break.

The tail is where this shows up. Average day, nobody notices. The one day the API is down, the fallback is the whole story. That’s p99 behavior again - the rare case is the one that decides whether your system holds up or quietly lies to your users.