BlogEngineering

ENGINEERING

Fail-closed means the failure is silence, not damage

Six exact strings open the outbound gate. Every other value on earth, including a typo of the word true, leaves it shut. That choice has a real cost.

SAGARISEngineering8 min
Fail-closed means the failure is silence, not damage

The entire polarity decision is one set of six environment tokens that open the gate: 1, true, on, yes, open and enabled. The comment sitting above it states the rule the set implies: everything else, unset, empty, the string false, or garbage, is closed.

Six exact strings, compared after trimming and lowercasing. That is the complete set of values that will let this system send anything to a real person. A flag set to ture is closed. A flag set to 0 is closed. Unset is closed. An empty string is closed. A value that arrived garbled from a template is closed.

Write the same flag the other way round, as a check for the string "false", and every one of those cases sends.

The two failure modes are not the same size

Every boolean read from a configuration file has a failure mode, because configuration goes wrong. Variables get dropped in a template render. A deploy script writes an empty string. Somebody renames a substitution in one file and not the other.

The question is not whether that happens. It is what the system does on the day it does.

A gate that fails open sends email to real people because of a typo. A gate that fails closed sends nothing because of a typo. Both are bugs, both are embarrassing, and only one of them can be undone. The recipient of a mistaken send has already read it. There is no rollback for a message that arrived, and there is no version of an apology that removes it from a spam complaint, a suppression list, or the inbox of somebody who was on a do-not-contact list.

So the polarity is not a style preference. It is a choice about which of your bad days is recoverable.

The failure mode you are choosing is silence, and silence is hard

The honest half of this argument is the part most write-ups skip. Fail-closed does not eliminate the failure. It changes it into one that is quiet, and quiet failures have their own pathology.

A system that sends nothing looks exactly like a system with nothing to send. There is no error in a log, no exception, no red build. Everything is green because everything declined to act, and declining to act is what the code was asked to do. If your operational picture is "no alerts", a shut gate is invisible in it.

That is a real cost and it has to be paid for with a status surface. Something has to be able to answer, out loud, "the gate is currently shut, here is which scope, here is the variable that would open it". The gate's own decision object is built for that: when it refuses it returns a structured refusal carrying the scope, a machine-readable reason, and a message naming the exact environment variable an operator would set, rather than a bare false that tells the caller nothing.

There is a second cost, and the same codebase has already paid it once. A different control, the do-not-call verification path, was originally repaired toward a naive fail-closed default, and the design note records the problem: fail-closed on an unprovisioned registry would block every contact, and a control that blocks everything is a control that gets ripped out within a week. The fix was a third state. It refuses only when a provider is configured and has returned no verdict, and the escape hatch retires itself by derivation the day a real provider is configured, so there is nothing for anybody to remember to delete.

Fail-closed is correct when the closed state is operable. When the closed state makes the product unusable, fail-closed is just a control on its way to being deleted. The outbound gate can be closed indefinitely without breaking anything a person needs day to day, which is what earns it the strict polarity.

The mechanism that makes that true is a carve-out written into the module: transactional mail, meaning authentication, magic links and receipts, does not flow through this gate at all. Only non-transactional mail does. Closing the gate stops the sequences and never locks the team out of their own application.

Closed in the code is only half of it

A fail-closed default in library code is one line away from being irrelevant, because the deploy can set the variable to true and the code will do exactly what it was told.

So the second half of the design is a committed contract test that reads other files in the repository and asserts they agree. It opens nine of them: the build config, the deploy shell script, two Terraform files, the environment example, two READMEs, and two runbooks. It covers three variables: the master outbound switch, the email channel switch, and the flag that would allow real recipients rather than a test redirect.

For each one it asserts the same shape in every place the variable can be expressed. The build substitution is explicitly "false", not merely absent. The deploy script passes it through by name. The Terraform module references it exactly twice. The Terraform variable block declares a default of false. The environment example writes it as false.

This is the useful pattern, and it generalises past outbound sending. A default is not a property of a file. It is a property of the agreement between a code path, a build config, an infrastructure module and the documents that tell a human what the value is. Any one of those can drift on its own, and drifting is silent, because each file is internally consistent and no compiler reads across them.

The detail worth stealing is about a stale row in a README

Two of the assertions in that test are not about the deploy at all. They are about documentation, and the reasoning is recorded inline.

Asserting that a README contains a variable name is satisfied by any occurrence of it. If the file has two rows for the same variable, one accurate and one left over from an earlier design, the assertion passes on the strength of the stale row. The comment names that as how a polarity drift survived review once, on this exact system.

So the test asserts each of those variables appears in exactly one README row. One row per variable means the accurate description is the only description available. And it adds a negative assertion, that the README must not tell an operator the redirect target has a library-code default, because it no longer has one.

That is a test about prose, enforced by a build. It is also the smallest possible version of a rule worth applying everywhere: a document that describes a default is part of the control, and a document nothing checks will eventually describe a control that no longer exists.

The same test insists the redirect target itself lives in the deploy path with a non-empty value, rather than sitting as a constant in library code, on the stated grounds that staging or restoring the interlock should be reviewable in a diff.

The subtle scope, and why it exists

The gate has four scopes: email, SMS, voice, and one called enrollment. The first three are obvious. The fourth is the one that shows somebody thought past the demo.

Enrollment is the arming of a send, meaning writing or reactivating the rows that a scheduler will later act on. It follows the master flag only, and the module says why: arming sends while outbound is globally closed would just build a latent backlog that fires on the first scheduler tick after the gate opens.

That is the failure this design is really guarding against, and it is not a leak. It is a flood. A gate that only blocks the final send lets a month of queued work accumulate behind it, and the moment somebody opens the gate for a small deliberate test, all of it goes out at once. Blocking the send without blocking the arming converts a safety control into a pressure vessel.

What this article does not claim

It does not say what any running environment is currently set to. The evidence base this was written from lists deployed flag values as unverified, and names the command that would settle them. That command was not run here, and per-environment gate states are not something to publish anyway.

It does not say the closed default means no message can ever reach a person. It means the default in code and in the committed deploy configuration is shut, that opening it is a deliberate, reviewed, per-environment act rather than the standing posture, and that a committed test fails the build if any of those nine files stops saying so.

And it does not say external do-not-call registry scrubbing is running, because it is not provisioned on the committed deploy path. The verification code says that in its own output rather than returning a clean result, which is the same discipline in a different place. Internal do-not-contact, suppression and consent gates are live.

The claim that survives all of that is narrow and checkable: the polarity is strict, it is written down in nine places, and a test reads all nine.

The question worth asking a vendor whose product can contact your customers: when your send configuration is wrong, does your system send the wrong thing or nothing at all, and which file would I read to find out?

SAGARIS

Written by the SAGARIS team.

See the engine run on your pipeline.

Thirty minutes, your own data, no setup.

Book a demo

Get the next one in your inbox.

SAGARIS opens fully in October 2026. Join the waitlist and we will be in touch before launch.

We use these details to contact you about SAGARIS. See our privacy policy.

Book a demo