November 18, 2025

A Cloudflare engineer changes a permission in a ClickHouse database. Routine. Unremarkable. The kind of thing that happens dozens of times a day at any infrastructure company.

That change caused a feature flag file to double in size. The file propagated to every machine in Cloudflare’s global network—over 330 cities, millions of requests per second. A Rust binary on those machines hit .unwrap() on a value it couldn’t parse. Panicked. Crashed.

For roughly three hours, a significant chunk of the internet broke.

No vulnerability. No attacker. No dramatic zero-day. Just a config file that got too big for a program that never expected it to.


What Actually Happened: The Failure Chain

Cloudflare runs a feature flag system—a standard tool for enabling or disabling product behavior dynamically, without a code deploy. Feature flags are config. They’re meant to be lightweight, fast to update, and globally consistent.

The feature flag configuration lives in a ClickHouse database. On November 18, a permission change to that database had an unintended side effect: it caused the exported feature flag file to include significantly more data than intended. The file roughly doubled in size.

That file gets pushed to every machine in the network on a regular cadence—every few minutes. So within minutes, every machine had the bloated file.

PLEASE ADD AN ARCHITECTURE DIAGRAM HERE TO MAKE IT BETTER

On each machine, a Rust-based daemon reads the feature flag file and applies its contents. That daemon used .unwrap() to handle the deserialization result. In Rust, .unwrap() on an Err value doesn’t return an error gracefully—it panics. The process terminates.

The daemon died on every machine, nearly simultaneously. And because that daemon was load-bearing—other systems depended on it being alive—the cascading failures followed.

The full chain:

  1. Permission change in ClickHouse → unintended schema/data exposure in export
  2. Bloated config file generated
  3. File propagated globally via automated push
  4. Rust daemon calls .unwrap() on malformed or oversized input
  5. Panic. Process death. No graceful degradation.
  6. Dependent services fail. Traffic drops. Errors propagate outward.

Each step in that chain had an implicit assumption baked in. Every assumption broke.


The .unwrap() Problem Is Not Really About Rust

Let me be precise here, because this is where most post-mortems get shallow.

Blaming .unwrap() is like blaming a fuse for a power surge. The fuse did its job—it broke. The problem is that it broke everywhere at once, with no circuit breaker above it.

In Rust, .unwrap() is a conscious choice. You’re saying: “I’m confident this will succeed. If it doesn’t, crash loudly.” That’s sometimes the right call. In a controlled, bounded context where the input is guaranteed, it’s fine. But a config file pushed from an external system—one that can change shape due to unrelated database operations—is not a controlled, bounded input.

// This is what breaks:
let config = parse_feature_flags(&raw_bytes).unwrap();

// This is what should exist at a system boundary:
let config = match parse_feature_flags(&raw_bytes) {
    Ok(cfg) => cfg,
    Err(e) => {
        log::error!("Failed to parse feature flags: {}. Retaining last known good config.", e);
        return last_known_good_config;
    }
};

The fix isn’t “don’t use .unwrap().” The fix is: at any boundary where external data enters your system, you do not trust the shape of that data. You validate. You fallback. You never panic on production input you didn’t generate yourself.

This is a discipline problem, not a language problem.

ADD PICTURE HERE TO MAKE IT ENGAGING


Configuration Is Not Static Data

Here’s the mental model that causes these outages: teams think of configuration as data. Inert. Safe. Low-risk compared to code.

It isn’t.

When you deploy config to 300+ machines every few minutes, you’re running a continuous deployment pipeline. Config changes have the same blast radius as code changes—sometimes larger, because they move faster and with less ceremony.

Cloudflare’s feature flag system pushes updates globally in minutes. That’s the whole point—fast, dynamic behavior control. But fast propagation of a corrupt or malformed file means fast propagation of the failure.

Compare how teams treat code vs. config:

ConcernCode DeployConfig Deploy
Review processPR review, CI gatesOften direct write
Schema validationCompiler, type systemFrequently absent
Staged rolloutCanary → % traffic → globalOften immediate global push
Rollback mechanismGit revert, deployment toolingSometimes manual
Size/shape constraintsEnforced by type systemRarely enforced
MonitoringDeployment metrics, error ratesFrequently blind

Most teams have invested heavily in making code deploys safe. Config deploys get a fraction of that rigor. This is the architectural blind spot.


The Implicit Assumptions That Broke

Every system has load-bearing assumptions—things that are never written down because “of course that’s true.” Cloudflare’s outage exposed several simultaneously.

Assumption 1: The feature flag file has a stable, bounded size.

Nobody wrote this down. Nobody tested what happens when it doubles. The Rust binary was never asked “what do you do with a 2x larger input?” It just assumed the input would look like it always had.

Assumption 2: A database permission change only affects access control.

Permission changes feel safe. They don’t modify data logic; they modify who can see what. But permissions governing what data gets exported are functionally schema changes. That distinction wasn’t respected.

Assumption 3: The config parsing layer handles errors gracefully.

It didn’t. .unwrap() in a hot path with external input means the process treats any parse failure as unrecoverable. That assumption was baked into the code and never revisited.

Assumption 4: The rollout can be fast because config changes are low-risk.

Speed without staged validation is how a local problem becomes a global one in five minutes.


Why This Pattern Exists Everywhere

I want to be direct: this isn’t a Cloudflare-specific failure. The architecture that produced this outage exists in some form at nearly every company operating infrastructure at scale.

Feature flags, A/B test configs, routing rules, rate limit tables, allowlists, blocklists—all of these are config. All of them are often pushed with less validation than a code deploy. All of them are read by production binaries that were written assuming the config looks a certain way.

Working at Cox Communications, I’ve seen firsthand how operational changes—changes that feel procedural and low-stakes—become the actual root cause of major incidents. A CMTS config push. A DHCP scope modification. A routing policy update applied globally instead of regionally. The pattern is identical: someone makes a bounded, well-intentioned change; a downstream system had an implicit assumption about the world; the assumption breaks; the blast radius is disproportionate to the original action.

The reason it keeps happening:

  • Config systems evolve faster than their consumers. The database schema or export format changes; the binaries reading the output don’t get updated simultaneously.
  • Validation lives at the wrong layer. Teams validate that config is syntactically correct. They don’t validate that it’s within the operational envelope the consuming binary was written to handle.
  • Blast radius is invisible until it isn’t. Nobody builds a dashboard for “what happens if this config file is 2x larger?” Because nothing’s ever been 2x larger before.

What Robust Config Management Actually Looks Like

Cloudflare will fix their specific issue. The .unwrap() will become a match. Size limits will be enforced at the export layer. The permission change workflow will get more gates.

But the structural fixes are the ones worth focusing on.

1. Validate at the producer, not just the consumer.

Before the config file leaves the generation system, assert invariants: size bounds, required fields, schema version compatibility. If the export is anomalous, stop the push. Don’t propagate the problem.

2. Canary config rollouts.

Push to 1% of machines. Measure error rates, process health, dependent system behavior. If metrics move, halt. This is standard practice for code. It should be standard for config.

3. Consumers must have a fallback path.

Any binary reading external config should have a “last known good” state. If parsing fails, log loudly and continue with the previous valid config. Panicking is never the right answer for production input at a system boundary.

4. Config changes need the same change management as code.

This doesn’t mean slowing everything down—it means applying the same types of gates: automated schema validation, size checks, staged rollout, automated rollback triggers. Speed comes from automation, not from skipping steps.

5. Decouple config propagation from config application.

Receiving a new config file and applying it are two separate operations. Receive it everywhere, validate it at a fleet-wide level, then apply it with rollout controls. This window—between receipt and application—is where you catch problems before they’re global.

PLEASE ADD AN ARCHITECTURE DIAGRAM HERE TO MAKE IT BETTER


The Deeper Issue: Operational Changes Are Code Changes

Here’s the stance I’ll take directly: the industry systematically underinvests in the safety infrastructure around operational changes because they don’t look like code.

A permission change in ClickHare looks like an administrative action. A feature flag update looks like flipping a switch. A routing table edit looks like network management. None of these feel like “deploying software.” But at scale, they all have the same properties as software deployments: they change the behavior of running systems, globally, with real user impact.

The mental model needs to change before the tooling will.

Every system that reads external config is making an implicit contract with the system producing that config. Both sides need to honor it. When one side changes—even accidentally—the other side needs to be resilient enough to not crash. And the delivery mechanism between them needs to be slow enough that a bad change can be caught before it’s everywhere.

Cloudflare is transparent about their incidents in a way most companies aren’t—their public post-mortem is detailed and honest. The specific root cause matters less than the pattern it reveals. The next company to have this outage probably won’t be running Rust. The file that breaks them probably won’t be a feature flag. But the shape of the failure will be identical: a small, unassuming change; a downstream system with a hard assumption; a propagation mechanism that’s fast because speed is the feature; no validation in between.


Takeaway

Configuration management is load-bearing infrastructure. Treat it that way.

Concretely:

  • Every config consumer needs a fallback state. Parse errors should never be fatal.
  • Every config producer needs to validate output invariants before propagation.
  • Global, fast rollouts need staged deployment controls—even for “just config.”
  • Operational changes—permissions, schema adjustments, database reconfigurations—need the same impact analysis as code changes when they affect exported data.

The November 2025 Cloudflare outage wasn’t caused by a bug in the traditional sense. It was caused by the gap between what a system was designed to handle and what it was actually given. That gap exists in your infrastructure too. The question is whether you find it in a post-mortem or a production incident.


Sources: Cloudflare Blog — November 18, 2025 Outage Post-Mortem