A new core for Cloudflare's request path
Every request that enters Cloudflare's network begins a journey through a series of systems: first the HTTP and TLS termination layer, then a component we call FL, and finally Pingora for cache lookups or origin fetches. FL has always been the heart of this flow — it applies each customer's configuration, enforcing WAF rules, DDoS protection, and routing decisions before traffic continues on its way.
FL was built more than 15 years ago, with the first commit made by founder Lee Holloway nine months before Cloudflare's initial launch. It has served as the platform for an ever-growing catalog of products, but that growth came at a cost. As features accumulated, FL became harder to maintain and slower to process requests. Each new addition required careful checks against all existing logic, and every feature added a little more latency to the request path.
Recently, we completed a major overhaul of this system. Rebuilding the core request-processing engine has cut the median response time by 10ms and delivered a 25% performance improvement, as measured by third-party CDN performance tests. The new implementation also improves security and shortens the time required to build and ship new products.
From Lua to a Rust-based architecture
The original FL went through several iterations. Its first version ran on NGINX with product logic written in PHP, but after three years the system became too complex and too slow, prompting a near-complete rewrite. That second version, committed by Dane Knecht (now Cloudflare's CTO), was built on NGINX with the OpenResty framework and LuaJIT.
That stack served well for years, but it eventually showed its age. LuaJIT had obscure bugs that consumed increasing amounts of engineering time, and the flexible but unstructured Lua codebase made it difficult to integrate new product logic safely. Adding a new product meant auditing all existing ones to check for potential interference.
In July 2024, we started a new implementation, called FL2 — the original system retroactively became FL1. FL2 is written in Rust and built on Oxy, our internal framework for constructing high-performance proxies. The choice was deliberate: Rust's compile-time guarantees eliminate entire categories of bugs that affected FL1, such as memory safety issues and data races, while delivering performance comparable to C. And Oxy, which already powers services like Zero Trust Gateway and Apple's iCloud Private Relay, brought proven experience handling diverse traffic patterns at scale.
Restarts without dropped connections
One of the most significant improvements in FL2 is how it handles software updates. FL1 required full process restarts to deploy new versions, which immediately terminated any active connections. That was especially disruptive for long-lived sessions such as WebSockets, streaming, and real-time APIs.
Oxy includes built-in support for graceful restarts. When a new instance starts, the old one stops accepting new connections but continues serving existing ones until they end naturally. This means a WebSocket session survives a deployment without interruption. Fleet-wide rollouts are orchestrated over several hours, making the aggregate effect nearly invisible to users.
FL2 goes further with systemd socket activation. Rather than having each proxy manage its own sockets, systemd creates and owns them, decoupling socket lifetime from the application's lifetime. Even if an Oxy process crashes or restarts, the sockets remain open, ready to accept connections as soon as the new process is running. This eliminates the connection-refused errors that could occur during FL1 restarts.
Additionally, we replaced the Go-based tableflip library with our own Rust coordination mechanisms, called shellflip. It uses a restart coordination socket that validates configuration, spawns new instances, and confirms the new version is healthy before the old one shuts down. This provides immediate feedback to automation tools when failures occur.
Structured modules with enforced rules
FL1's problems largely stemmed from the lack of structure in its product logic. To avoid repeating that mistake, FL2 introduces a strict module system that separates all product functionality into well-defined modules with enforced boundaries:
- Modules cannot perform any input or output operations themselves.
- Each module provides a list of phases.
- Phases are evaluated in a strictly defined order, identical for every request.
- Each phase declares its required inputs and the outputs it may emit.
These rules are enforced at compile time. A module requesting input from another product must explicitly declare that dependency. For example, a module for custom error pages might take visitor IP information, HTTP headers, and a module value produced by the rulesets-based custom errors product. These explicit contracts make it immediately clear which products affect which others, and they allow modules to pass information to each other through well-defined module values.
Despite the strictness of the framework, we've been able to implement all existing product logic within it. The approach replaces the implicit interdependencies of FL1 with explicit, verifiable interfaces, making the system easier to extend and reason about as new features are introduced.
Rolling out FL2 without stopping development
Migrating a 15-year-old codebase that powers Cloudflare's products without halting feature development required a way for teams to adopt Rust gradually. Rather than asking engineers to maintain parallel implementations in Lua and Rust, Cloudflare built a compatibility layer inside its NGINX and OpenResty-based system that could execute the new Rust modules directly. Teams could then replace their Lua logic incrementally, without waiting for the full system rewrite to land.
A sample from the custom error page module shows how this works in practice:
pub(crate) fn callback(_services: &mut Services, input: &Input<'_>) -> Output {
// Rulesets produced a response to serve - this can either come from a special
// Cloudflare worker for serving custom errors, or be directly embedded in the rule.
if let Some(rulesets_params) = input
.get_module_value(MODULE_VALUE_RULESETS_CUSTOM_ERRORS_OUTPUT)
.cloned()
{
// Select either the result from the special worker, or the parameters embedded
// in the rule.
let body = input
.get_module_value(MODULE_VALUE_CUSTOM_ERRORS_FETCH_WORKER_RESPONSE)
.and_then(|response| {
handle_custom_errors_fetch_response("rulesets", response.to_owned())
})
.or(rulesets_params.body);
// If we were able to load a body, serve it, otherwise let the next bit of logic
// handle the response
if let Some(body) = body {
let final_body = replace_custom_error_tokens(input, &body);
// Increment a metric recording number of custom error pages served
custom_pages::pages_served("rulesets").inc();
// Return a phase output with one final action, causing an HTTP response to be served.
return Output::from(TerminalAction::ServeResponse(ResponseAction::OriginError {
rulesets_params.status,
source: "rulesets http_custom_errors",
headers: rulesets_params.headers,
body: Some(Bytes::from(final_body)),
}));
}
}
}
The design keeps each module's internal logic cleanly separated from data handling, with Rust's explicit error handling model baked into the interface. This approach let Cloudflare's most actively developed modules move to the new system quickly while the engineers behind them kept up their normal release cadence.
Testing at scale
Cloudflare built a test framework called Flamingo to validate the migration. It runs thousands of full end-to-end test requests concurrently against production and pre-production systems, exercising both the legacy FL1 platform and the new FL2 platform with identical tests to catch behavioural differences.

Every deployment flows through staged rollouts with gradually increasing traffic. Each stage runs the full test suite automatically, and only passes if performance and resource usage stay within bounds. Failures pause or revert the rollout. The result: new features can ship on FL2 within 48 hours, compared with weeks on the old platform. At least one feature announced this week went through that pipeline.
Fallbacks and comparisons
With over 100 engineers and 130 modules involved, FL2 isn't finished yet. To route production traffic to an incomplete system, Cloudflare implemented a fallback mechanism: if FL2 receives a request or configuration it cannot handle, it forwards the raw bytes to FL1 at the network level. This lets traffic flow to FL2 even for features that aren't ported.
Fallbacks provide a second benefit beyond safe incremental rollout. When a feature has been ported to FL2, engineers can evaluate it there, trigger a fallback, and compare both systems' responses side by side. This gives high confidence that the Rust implementation behaves identically to the original.
Customer traffic started reaching FL2 in early 2025, beginning with free customers. Cloudflare Community MVPs served as early smoke testers, flagging issues that might point to the new platform. From there, paid customers were added gradually, with some of the largest customers onboarded early in exchange for feedback. Most customers are now on FL2, though a few features remain unfinished. Cloudflare targets shutting down FL1 within a few months.
Why FL2 is faster
The performance gains come primarily from doing less work. Each module can declare filters that control whether it executes for a given request:
filters: vec![],
Instead of running product logic for every product on every request, FL2 selects only the required modules. The incremental cost of adding a new product has effectively disappeared.
FL1's architecture compounded the problem: NGINX written in C, LuaJIT binding layers, and Rust modules meant constant data conversion between language representations. FL2 consolidates everything into a single Rust codebase. Cloudflare's internal measurements show FL2 uses less than half the CPU and substantially less memory than FL1.
Performance and security validation
Rollout measurements using Cloudflare's own tools and the independent CDNPerf benchmark showed websites responding 10 ms faster at the median, a 25% performance improvement.

Security also improves with the language change. Rust's compile-time memory checks and type system eliminate entire classes of errors that LuaJIT could not catch. The rigid module system means most changes carry high confidence. However, Rust alone isn't sufficient — unsafe code can still corrupt memory. Cloudflare maintains strict compile-time linting, coding standards, testing, and review processes to mitigate that risk.
The company's policy of investigating every unexplained crash as a high priority stays in place, though FL2's crash rate is dramatically lower. Novel crashes so far have traced mostly to hardware failures, leaving more time for thorough investigations when they do occur.
Remaining migration work
Cloudflare plans to finish migrating off FL1 by early 2026, and one service still needs the full treatment: the HTTP & TLS Termination box shown in the original system diagram remains an NGINX service. A Rust rewrite is already underway and expected to complete early next year.
Once everything runs as modular Rust components with solid testing and scaling, Cloudflare can begin optimizing the system itself — reworking how modules connect, adding support for non-HTTP traffic such as RPC and streams, and simplifying the overall architecture.



