CDN Engineering

The Fastly VCL == false Trap

How using == false instead of ! in Fastly VCL compound conditions can silently break your logic

Laurent Goudet · February 10, 2026 · 7 min read

The Bug

A Fastly VCL condition with two == false checks in an && chain was silently evaluating to true, bypassing a domain filter. The condition was supposed to restrict logic to a specific domain, but it was being applied to all traffic.

The engineer spent hours debugging, isolating each sub-condition individually. Each condition worked correctly on its own. The bug only manifested when

two or more == false comparisons

were chained together with &&.

Broken

Two == false in a && chain — condition always true
if (req.http.X-Bot == false
    && var.is_blocked == false
    && req.http.host ~ “example.com”) {
  // Domain-specific logic
  // BUG: always entered!
}

Fixed

Replace one == false with ! — condition works correctly
if (req.http.X-Bot == false
    && !var.is_blocked
    && req.http.host ~ “example.com”) {
  // Domain-specific logic
  // Works correctly
}

The Investigation

We dug into Fastly’s VCL documentation looking for an explanation. What we found was surprising:

Fastly’s operator precedence table only lists four operators

: (), !, &&, and ||. Comparison operators like ==, !=, ~, and > are documented in a separate section, without any defined precedence level.

In contrast, the open-source Varnish VCL parser (which Fastly’s VCL is derived from) has a clear recursive descent hierarchy where == binds tighter than &&. The omission from Fastly’s documentation raised the question: does Fastly’s fork handle operator precedence differently?

Finding: Missing Operator Precedence

Fastly’s official VCL reference documents operator precedence as a four-level table: ()!&&||. Comparison operators (==, !=, ~, >, <, etc.) are absent from this table entirely.

In Varnish’s open-source parser (vcc_expr.c), the recursive descent functions follow a strict hierarchy: vcc_expr_cmp (comparisons) is called from vcc_expr_not, which means == naturally has higher precedence than &&. If Fastly’s fork deviates from this hierarchy, a == false && b could be misparsed as a == (false && b).

Parse Tree Comparison

How operator precedence changes the meaning of the same expression

Expected

== binds tighter than &&
&&
==
X-Bot
false
==
blocked
false

Actual (Fastly)

&& may bind tighter than ==
==
==
X-Bot
&&
false
blocked
false
AspectVarnish (open-source)Fastly VCL
Parser typeRecursive descent (vcc_expr.c)Proprietary fork
== precedence vs &&✓ Higher (well-defined)✗ Undocumented
Precedence tableImplicit in parser hierarchyOnly lists () ! && ||
Cross-type ==Strict type checking“Equality defined for all types”

Truth Table: Expected vs. Actual Results

Three of four input combinations produce wrong results
X-Botis_blockedExpectedActual
falsefalsetruetrue
falsetruefalsetrue
truefalsefalsetrue
truetruefalsetrue

The Proof

We deployed a dedicated PoC on a live Fastly service to confirm the bug. The test endpoint runs four variations of the same && chain where all three conditions should evaluate to false — the third condition checks req.http.Host == “www.nonexistent-domain.example, which can never match. None of the blocks should execute.

$ curl -sD- https://www.syd1.fln-dev.net/_vcl-bug-test{“\n”} x-test1-both-eqfalse: REACHED-BUG x-test2-both-bang: not-reached x-test3-mixed: not-reached x-test4-parens: not-reached

The four tests and their results:

Test

Pattern

Result

1

STRING == false && BOOL == false

REACHED-BUG

2

!STRING && !BOOL

not-reached

3

STRING == false && !BOOL

not-reached

4

(STRING == false) && (BOOL == false)

not-reached

Test 1 confirms the bug: two == false comparisons in a && chain cause the entire condition to evaluate to true, regardless of subsequent conditions. Tests 2-4 show three independent workarounds: using ! for both (Test 2), mixing == false with ! (Test 3), or wrapping each comparison in explicit parentheses (Test 4).

Finding: Precedence Alone Doesn't Explain It

If == and && share the same precedence and bind left-to-right, the expression A == false && B == false && (C == D) parses as ((((A == false) && B) == false) && (C == D)). Walking through the evaluation: A == false → true, true && B → false, false == falsetrue — but then true && (C == D) → false. The final && should bring it back to false. Yet the PoC shows it evaluates to true.

This points to a short-circuit code generation issue on top of the precedence problem. Compilers commonly optimize X == false in a && chain by inverting the branch condition — instead of computing the comparison and branching on the result, they evaluate X directly and flip the jump polarity. With two == false in the same chain, the second inversion may corrupt the jump target, causing the remaining conditions to be skipped entirely and execution to fall through to the body. This is consistent with all four PoC results: the bug requires exactly two == false in the chain (Test 1), a single one works fine (Test 3), ! uses a different code path entirely (Test 2), and explicit parentheses force the comparisons to be pre-evaluated before entering the short-circuit chain (Test 4).

Finding: Cross-Type Comparison

HTTP headers in VCL are strings. Writing req.http.X-Bot == false compares a STRING to a BOOL. Fastly’s documentation states “equality is defined for all types” but does not specify what happens when the types differ.

Does “0” == false evaluate to true? Does "" == false? What about a missing header? Varnish’s type system is strict, but Fastly’s fork may handle cross-type comparisons with implicit coercion. This undocumented behavior, combined with the precedence issue, creates a minefield for compound conditions.

The Fix

Replacing just one of the two == false with ! was enough to fix the bug. The final condition uses !var.is_blocked instead of var.is_blocked == false, while keeping the HTTP header comparison as req.http.X-Bot == false.

Explicit parentheses around each comparison also work: (req.http.X-Bot == false) && (var.is_blocked == false) forces the parser to group the comparisons correctly, preventing the precedence issue.

Note: The “fixed” code still uses req.http.X-Bot == false, which the Key Takeaways below flag as bad practice (comparing a string header to a boolean). This was the minimal hotfix — replacing one == false to break the compound precedence issue. The recommended practice is to use ! everywhere: !req.http.X-Bot && !var.is_blocked.

Key Takeaways for Fastly VCL

Use ! instead of == false.

The negation operator has well-defined precedence in Fastly’s documentation. The equality operator does not.

Don’t compare HTTP headers to booleans. Headers are strings. Use !req.http.X-Bot (tests for existence/truthiness) rather than req.http.X-Bot == false.

Be extra cautious with compound conditions. The bug only manifested with two == false in a chain. Single uses appeared to work fine, making this extremely hard to catch in testing.

Confirmed with a live PoC on Fastly’s VCL platform in February 2026. The bug has been reported to Fastly support. The likely root cause is undefined operator precedence for == combined with a short-circuit code generation issue when multiple == false comparisons appear in the same && chain. Exact confirmation pending from Fastly.

Frequently Asked Questions

Why does == false break in Fastly VCL compound conditions?

Fastly's VCL documentation doesn't define precedence for the == operator relative to &&. When two == false comparisons are chained with &&, the condition may be misparsed.

How do you negate a condition in Fastly VCL safely?

Use the ! operator instead of == false. The ! operator has well-defined precedence in Fastly's documentation, while == does not.

Can you compare HTTP headers to booleans in VCL?

Headers are strings in VCL. Comparing a string to a boolean (req.http.X-Bot == false) relies on undocumented cross-type coercion. Use !req.http.X-Bot instead.

Laurent Goudet

CTO at Freelancer.com

AI agents, networking, and infrastructure at scale

Other deep-dives

© 2026 Laurent Goudet · Bordeaux, France · lepro.dev

vd9714f4