Entry 015Design8 min readBy Mandare Labs

Fail closed or fail open: what an AI gateway should do

When the system that checks an agent's budget is unreachable, a gateway either lets the request through (fail open) or refuses it (fail closed). Defaults differ: Envoy's failure_mode_allow is false, LiteLLM's fail_closed_budget_enforcement is off, and Stripe Issuing applies a timeout setting you choose. For anything that moves money, refuse, make the refusal loud, and decide each dependency on purpose.

A failed check is a decision you make before it fails

An agent gateway sits between an agent and something that costs money: an LLM provider, a card, a paid API. Before it forwards a request it asks other systems questions. Is there budget left? Does policy allow this? Can the decision be written down? Is a human required?

Every one of those systems can be unreachable. The gateway then has two choices, and both are a policy:

Neither is free. Fail open buys availability and pays for it in the case a limit exists to prevent: a looping agent during an outage. Fail closed buys the limit and pays in stopped work. The mistake is not choosing one. The mistake is never choosing, and shipping whatever the software defaulted to.

Three systems with documented defaults show how different those are.

Envoy rejects by default and lets you flip it per filter

Envoy's external authorization filter asks a separate service whether to admit a request. Its API reference, read on 15 October 2026, documents the failure setting directly. With failure_mode_allow set to true, the filter "will accept the client request" even if communication with the authorization service has failed or it "has returned an HTTP 5xx error". With false, it "will reject client requests and return Forbidden" in the same cases. The same text then says "Defaults to false."

# Fragment of an envoy.filters.http.ext_authz filter configuration
failure_mode_allow: false    # the documented default: reject when the check fails

Two neighbouring fields are worth copying as ideas. status_on_error sets the status returned "when the authorization server returns an error or cannot be reached", with a default of 403. And when failure_mode_allow and failure_mode_allow_header_add are both true, Envoy adds x-envoy-auth-failure-mode-allowed: true to the request headers. A request that skipped its check carries a label saying so. Anything downstream can count those requests or refuse them.

Stripe Issuing gives you two seconds and a fallback you configure

Stripe Issuing asks the card issuer's own endpoint to approve or decline each authorization in real time. Stripe's documentation on real-time authorizations, read on 15 October 2026, states the budget: "If Stripe doesn't receive your approve or decline response within 2 seconds, the Authorization is automatically approved or declined based on your timeout settings", or Autopilot settings if configured.

So the failure behaviour is a setting in your Stripe dashboard, not a property of your code. A webhook that is down, slow or answering with a bad API version does not produce an error page. It produces whichever decision you configured.

The page also carries a warning that applies to any fail-open setting. Autopilot, listed as public preview on the page, "provides fallback options that allow you to continue making real-time authorization decisions when your systems are down", and the monitoring section says: "Don't rely on approved to gauge integration health because Autopilot can approve on your behalf during an error condition." The page gives the signal to watch: request_history.reason set to webhook_error or webhook_timeout. An authorization with approved: true and webhook_error means the money moved without your check.

The lesson generalises. If you let a check fail open, you need a second signal that tells you it did. A dashboard that only shows approvals will look healthy during the outage.

LiteLLM is open in documented cases and closed behind a flag

LiteLLM's budgets and rate limits documentation, read on 15 October 2026, describes both directions. Without a database, "litellm_settings.max_budget fails open there rather than erroring", and "the global budget check is skipped and requests keep being served past the limit". Key, team and user budgets are not enforced either, because "virtual keys cannot be resolved without a database".

The closing switch is a setting under general_settings in config.yaml:

general_settings:
  fail_closed_budget_enforcement: true

With it enabled, "every budgeted request validates spend against the authoritative database before being admitted", and a request whose spend cannot be verified "is rejected with a 503 instead of being admitted on an unverifiable budget". The docs say it is off by default and recommend leaving it off to "keep healthy under-budget traffic entirely off the database". That is the trade stated plainly: the closed mode validates spend against the database on every budgeted request.

The mechanics of LiteLLM's key budgets are covered in LiteLLM budget per key. A gateway-side spend limit from another vendor, and what it does when it cannot count, is in Cloudflare AI Gateway spend limits.

Which way each dependency on a spend path should fail

The three systems above cover one kind of dependency each. An agent spend path usually has more. The table is a recommendation, not a quotation: the reasoning is that a dependency should fail open only if its absence cannot cause money to move that the limit would have stopped.

DependencyIf it fails openIf it fails closedSuggested
Budget counter storeCalls pass uncounted; a loop is unbounded until recoveryCalls stop until the store returnsClosed
Policy or authorization serviceScope and identity checks are skippedCalls stopClosed
Audit or ledger writeSpend happens with no recordCalls stopClosed
Price table (unknown model)Cost is unknown, so the cap cannot count itThe call is refusedClosed
Human approver unreachableOver-threshold actions run unapprovedOver-threshold actions wait or failClosed
Provider call, outcome unknownTreated as free; headroom reopensTreated as spent at the estimateCount it as spend
Read-only metadata lookupA stale labelA stopped readOpen, with a visible flag

Two rows deserve a note. The audit-write row is closed because spend with no record cannot be reconstructed later, and a record written before the action can still refuse it when the write fails. And the provider-timeout row is the quiet one: a request that timed out may still have been billed. Treating it as zero reopens budget headroom that was in fact spent.

Fail closed has a price, so make the price visible

A gateway that fails closed turns a dependency outage into an agent outage. Three habits keep that tolerable.

  1. Scope what closes. Close the spend path, not the whole service. Reads, health checks and the ability to stop an agent should not depend on the thing that is down.
  2. Return a distinct, countable error. A 503 with a stable code per cause is an alert source. Envoy's header and Stripe's webhook_timeout reason are the same idea from the other side.
  3. Watch for silent outages. A refusal caused by an unavailable ledger cannot be written to that ledger. The signal has to come from outside it: a health endpoint, a metric, a log line.

None of this needs a particular product. It needs the question asked per dependency, and the answer written next to the config.

Where Mandare fits, and the limits of the current release

Mandare's gateway (the "door") is written to fail closed on the spend path. This section applies to the CLI at v0.1.0, which npm view @mandarelabs/cli version reported on 15 October 2026, checked against commit 327442e of the public repository.

What the code and docs say, with the file:

The limits, next to the claims they limit:

For the shape of the whole model, the concepts page lists each door and what it checks.

Questions

What does fail closed mean for an AI gateway?

If the gateway cannot complete a check that guards spend (a budget lookup, a policy decision, an audit write), it refuses the request instead of forwarding it. The agent sees an error, not a free call. The cost is availability: while the check is down, the guarded calls stop.

Is Envoy's ext_authz filter fail open or fail closed by default?

Fail closed. Envoy's documentation says failure_mode_allow "Defaults to false", and with false the filter rejects client requests with Forbidden when the authorization service fails or returns a 5xx. Setting it to true accepts those requests.

Does LiteLLM fail open when its database is down?

For the global max_budget, yes. LiteLLM's documentation, read on 15 October 2026, says it "fails open there rather than erroring" without a database, and key, team and user budgets are not enforced either. The setting fail_closed_budget_enforcement: true rejects budgeted requests with a 503 when spend cannot be verified.

Should an agent's read-only calls fail closed too?

Not necessarily. The rule is about the spend path. A dependency whose failure can only delay a read can reasonably fail open, as long as the choice is written down and the skipped check is visible in logs or a header.

Sources

  1. ext_authz filter proto (Envoy API reference) · Envoy project
  2. Issuing real-time authorizations · Stripe
  3. Budgets, Rate Limits (LiteLLM proxy documentation) · LiteLLM

Run it yourself: 3 commands, no API keys.

github.com/mandarelabs/mandare →
← All journal entries