WAF visibility so you know what actually matched
The Cloudflare WAF applies layer 7 protection through managed rules, custom rules, rate limiting, and detections built on the Rulesets engine. When a request matches a rule expression, the engine runs the associated action. The Log action is useful for dry-running rules before enforcement, but it only proves a rule matched—not why, or which part of the expression triggered.
That gap matters because rule expressions can be compound. A match may come from the left-hand side of an OR, the right-hand side, or a transformation applied to a field before comparison. For example, a rule that checks whether any request header contains an authorization key, or whether the lowercase host header starts with cloudflare, leaves you guessing which condition fired and what the post-transformation values looked like.
any(http.request.headers[*] contains "authorization") or starts_with(lower(http.host), "cloudflare")
The ambiguity grows when multiple rules in a go ruleset register matches—take Cloudflare OWASP, which uses additive scores across rules until a threshold is crossed. Cloudflare Managed and OWASP rules also do not expose their internal expressions, so rule titles and tags are the only hints. A label like “SonicWall SMA - Remote Code Execution - CVE:CVE-2025-32819” tells you roughly what the rule covers, but not which field value in your request tripped it.
Payload logging as the diagnostic layer
Payload logging addresses exactly this. It records the specific fields and their values (after any transformations like base64 decoding, URL decoding, or lowercasing) from the rule that led to a match. Instead of treating a rule as a black box, you see the concrete request characteristics the engine evaluated.
For customers who need to fine-tune the WAF default configuration, this data is what turns a match into a decision point: is this a false positive, or is the rule catching genuine malicious traffic? The log output itself is accessible through Security Analytics, Security Events, Logpush, or Edge Log Delivery, so the drill-down can happen in the same toolchain you already monitor.
Inside the payload logging compiler
Wirefilter powers both Cloudflare's ruleset and payload logging engines. These are Rust objects that implement a compiler trait, which drives the compilation of abstract syntax trees (ASTs) derived from WAF expressions. When the Rulesets Engine evaluates an expression and it returns true, the expression and its execution context are passed to the payload logging compiler for re-evaluation. The execution context carries all runtime values needed to evaluate the expression, and the fields involved in branches that evaluate to true are logged as a map of wirefilter fields and their values (Map<Field, Value>).
struct PayloadLoggingCompiler {
regex_cache HashMap<String, Arc<Regex>>
}
impl wirefilter::Compiler for PayloadLoggingCompiler {
type U = PayloadLoggingUserData
fn compile_logical_expr(&mut self, node: LogicalExpr) -> CompiledExpr<Self::U> {
// ...
let regex = self.regex_cache.entry(regex_pattern)
.or_insert_with(|| Arc::new(regex))
// ...
}
}
These logs traverse Cloudflare's logging pipeline and can be read in multiple ways. Customers can configure a Logpush job to a custom Worker built by Cloudflare that uses their private key for automatic decryption, or use the payload logging CLI tool, Worker, or the dashboard. The logs are encrypted with the public key provided by the customer.

Precise logging for array fields
Some wirefilter fields are array types, such as http.request.headers.names, which contains all header names in a request.
[“content-type”, “content-length”, “authorization”, "host"]
Consider the expression any(http.request.headers.names[*] contains “c”). This evaluates to true if any header name contains the letter “c”. In the previous version of the payload logging compiler, all headers in the field would be logged because the entire field is part of an expression that evaluates to true:
http.request.headers.names[*] = [“content-type”, “content-length”, “authorization”, "host"]
The updated compiler partially evaluates array fields, logging only the indexes that satisfy the expression's constraint. In this case, the log now contains just the headers with a “c”:
http.request.headers.names[0,1] = [“content-type”, “content-length”]
Partial matches and operators
Wirefilter operators fall into two categories. Operators like eq produce exact matches, e.g., http.host eq “a.com”. Others like in, contains, and matches produce partial matches and work with regexes. The example above uses a contains operator, a partial match, and the any function, which also implies a partial match: if at least one header contains a “c”, only that header should be logged, not all of them.
The improved compiler handles contains similarly to the Rust standard library's “find” method for bytes, resulting in logs that show only the partial match:
http.request.headers.names[0,1] = [“c”, “c”]
This added precision also reduces log volume substantially. The request body (http.request.body.raw) is a frequently analyzed field that can be tens of kilobytes in size. When an expression looks for a regex pattern matching only three characters, the new compiler logs those three bytes instead of the entire body.
Context around matches
An array of matched characters alone isn't meaningful debugging data. The payload logging compiler now logs a “before” and “after” buffer (15 bytes each) around partial matches, providing context for what triggered the rule.
http.request.headers[0,1] = [
{
before: null, // isnt included in the final log
content: “c”,
after: “ontent-length”
},
{
before: null, // isnt included in the final log
content: “c”,
after:”ontent-type”
}
]
The improvement is significant. Previously, a payload log might contain all header values. Now, a log entry can show the exact index, the matched value (e.g., a malicious <script> tag), and surrounding context:

Optimization and truncation
Managed rules depend heavily on regular expressions to fingerprint malicious requests. Since these rules are written once and deployed across millions of zones, parsing and compiling the regexes is a CPU-intensive task. Cloudflare compiles these expressions once and caches them in memory, avoiding recompilation until the process restarts.
The payload logging compiler also uses dynamically sized arrays (vectors) for intermediate state, and crates like smallvec reduce heap allocations.
The “truncated” value seen in some payload logs appears when a firewall event exceeds its byte size limit. With the improvements, the p50 byte size of payload logs has shrunk from 1.5 kilobytes to 500 bytes — a 67% reduction — leading to far fewer truncated logs.

Next steps
Cloudflare currently uses a lossy UTF-8 representation for values, replacing non-valid UTF-8 strings (like multimedia) with U+FFFD replacement characters. Rules operating on binary data will require byte arrays or a different serialization format to preserve integrity.
Payload logging stores data as JSON. Cloudflare will benchmark it against binary formats like CBOR, Cap'n Proto, and Protobuf to reduce pipeline processing time. Binary formats also enable backward-compatible, defined schemas.
Payload logging currently only works with managed rules but will expand to other WAF products including custom rules, WAF attack score, content scanning, and Firewall for AI, which detects prompts containing PII:

The specificity improvements to payload logging give customers visibility into WAF behavior, ensuring their rules and configurations act as intended. Further reliability and latency improvements are planned, and since the changes break the JSON schema, Cloudflare has rolled them out incrementally with adequate documentation.



