Pages assembled with Edge Side Includes (ESI) can fail in ways that look like application bugs but are cache-layer problems. When MAIN.esi_errors increments, users receive partial pages with missing fragments or HTTP 500 responses from workspace exhaustion. MAIN.esi_warnings counts ESI tags that Varnish skipped, silently dropping content from otherwise valid responses.
Each <esi:include> tag in a backend response triggers a full sub-request through Varnish’s VCL pipeline. A page with five includes means six VCL cycles: the parent plus five children. Each sub-request consumes a worker thread for its full lifecycle, allocates workspace memory for headers and VCL processing, and can itself contain ESI tags that spawn further sub-requests. The recursion is bounded by max_esi_depth, but hitting that bound produces an error, not graceful degradation.
What this means
When ESI processing fails, symptoms fall into three categories:
Broken or partial pages (MAIN.esi_errors incrementing): The parent response was fetched successfully, but one or more ESI includes failed. The page is served with the failed fragment missing, or the entire response is synthesized as an error. Users see incomplete layouts, missing widgets, or blank sections.
Silently skipped tags (MAIN.esi_warnings incrementing): Varnish encountered ESI markup it could not parse and skipped the tag. The page renders but is missing the content that the skipped include would have provided. This is quieter than an error but equally broken from the user’s perspective.
Workspace pressure and 500 errors (MAIN.ws_client_overflow, MAIN.client_resp_500, MAIN.losthdr incrementing): ESI processing consumes workspace memory for each sub-request’s headers, VCL string operations, and response assembly. Pages with many includes, large response headers, or complex VCL can exhaust the per-request workspace, causing 500 errors. This is amplified when ESI fragments themselves carry large headers such as analytics cookies or Set-Cookie chains.
flowchart TD
A[Client requests page] --> B[Backend returns parent with ESI tags]
B --> C[Varnish parses ESI includes]
C --> D[Sub-request per include through VCL]
D --> E{Include fetch succeeds?}
E -->|No| F[esi_error: partial page served]
E -->|Yes| G{Depth or workspace limit hit?}
G -->|Yes| H[esi_error or ws_overflow: 500]
G -->|No| I[Assemble fragments, deliver page]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Failed included URL | esi_errors spikes; fragment endpoints returning non-200 | varnishlog -g request -q 'FetchError' to see failing backend fetches for fragment URLs |
| Circular includes | esi_errors increments; depth limit reached; pages never complete or timeout | Check whether fragment A includes fragment B which includes fragment A |
| Malformed ESI tags | esi_warnings increments; page renders with missing sections but no error | varnishlog -i ESI_xml -g request to see raw ESI markup and parse issues |
| Workspace exhaustion | ws_client_overflow or client_resp_500 increments on ESI pages; non-ESI pages unaffected | varnishstat -1 -f 'MAIN.ws_*_overflow' -f MAIN.client_resp_500 |
| Depth limit exceeded | esi_errors on deeply nested include hierarchies; correlates with specific page templates | Count ESI nesting levels in templates; compare to max_esi_depth |
| Version-specific error handling change | Behavior changed after upgrade; previously working pages now fail | Check Varnish version with varnishd -V; review esi_include_onerror feature flag |
Quick checks
# Check ESI error and warning rates
varnishstat -1 -f MAIN.esi_errors -f MAIN.esi_warnings
# Check workspace overflow indicators
varnishstat -1 -f 'MAIN.ws_*_overflow' -f MAIN.client_resp_500 -f MAIN.losthdr
# See ESI processing in the log stream
varnishlog -i ESI_xml -g request
<!-- TODO: verify ESI_xml is the correct VSL tag name for ESI processing events -->
# See ESI include failures with fetch context
varnishlog -g request -q 'FetchError'
# Check current max_esi_depth setting
varnishadm param.show max_esi_depth
# Check workspace sizes
varnishadm param.show workspace_client
varnishadm param.show workspace_backend
# Check thread pool pressure (ESI amplifies thread usage)
varnishstat -1 -f MAIN.threads -f MAIN.thread_queue_len -f MAIN.threads_limited
# Check backend health for fragment endpoints
varnishadm backend.list -p
# Inspect response codes for ESI sub-requests
varnishlog -i BereqURL -i BerespStatus -g request -q 'BerespStatus ne 200'
How to diagnose it
Confirm ESI is the problem. Check whether
MAIN.esi_errorsorMAIN.esi_warningsare incrementing at a rate that correlates with user-reported broken pages. If the counters are flat, the problem is elsewhere: backend, VCL, or storage.Identify the specific failure mode. Run
varnishlog -i ESI_xml -g requestduring the incident window. Look for malformed tags (correlates withesi_warnings) and recursive include chains where the same fragment URL appears at increasing depth.Trace failed includes. If
esi_errorsis incrementing, find which included URLs are failing. Usevarnishlog -g request -q 'FetchError'to see backend fetch failures. Common patterns: fragment endpoint returns 500 or 503 under load, fragment URL changed during a deploy, or the fragment endpoint requires headers that the ESI sub-request does not forward.Check for circular includes. If the same fragment URL appears at multiple nesting levels in
varnishlog, you have a circular dependency. Themax_esi_depthlimit will eventually fire, producing anesi_error. Circular includes are usually introduced by template refactoring or shared component includes that reference each other.Assess workspace pressure. Run
varnishstat -1 -f 'MAIN.ws_*_overflow' -f MAIN.client_resp_500. If these are nonzero and correlate with ESI traffic, workspace is too small for the header volume of your assembled responses. Checkworkspace_clientandworkspace_backendparameter values.Check version-specific behavior. Varnish 7.3 introduced strict ESI error handling where sub-requests returning non-200/204 status codes would fail the include. Varnish 7.5 reverted this: sub-requests are processed regardless of status code. The
esi_include_onerrorfeature flag controls this behavior, and its semantics inverted between 7.3 and 7.5. If you upgraded across these versions, check the flag state withvarnishadm param.show featureand verify your version withvarnishd -V.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
MAIN.esi_errors | Count of ESI processing failures. Each increment means a broken or partial page was served. | Any nonzero rate in production. |
MAIN.esi_warnings | Count of malformed ESI tags skipped. Content is silently dropped. | Sustained nonzero rate. |
MAIN.ws_client_overflow | Client workspace exhausted. ESI amplifies workspace consumption per request through sub-request headers and VCL operations. | Any nonzero rate, especially on ESI-heavy pages. |
MAIN.client_resp_500 | Delivery failures, often from workspace exhaustion during ESI assembly. | Spike correlating with ESI traffic. |
MAIN.losthdr | Headers dropped due to workspace limits. Can break caching (Vary lost) or authentication (Cookie lost). | Any nonzero value. |
MAIN.thread_queue_len | ESI sub-requests consume worker threads. Many includes means thread amplification proportional to include count. | Sustained nonzero, especially on ESI-heavy URLs. |
MAIN.backend_req | ESI misses generate additional backend requests. Fragment endpoints see amplified load. | Backend request rate higher than expected for the page count. |
MAIN.fetch_failed | Included URL fetch failures. Direct cause of many ESI errors. | Nonzero rate on fragment endpoints. |
Fixes
Fix failed included URLs
The most common cause of esi_errors in production. When an ESI fragment endpoint returns an error (500, 503, or timeout), the include fails and the page is broken.
- Identify the failing endpoint from
varnishlog -g request -q 'FetchError'. - Fix the backend issue, or ensure the fragment endpoint has its own health probe and error handling.
- If the fragment is non-critical, use
onerror="continue"in the ESI tag so the page renders without it rather than failing entirely. - Cache fragment responses with appropriate TTLs so transient backend failures do not break page assembly. Use
beresp.graceto serve stale fragments during backend hiccups.
Fix circular includes
Circular includes (fragment A includes fragment B which includes fragment A) hit the max_esi_depth limit and fail. This is a template bug, not a tuning problem.
- Identify the circular chain from
varnishlog -i ESI_xml -g request. - Break the cycle in the template layer. Shared components should not include their callers.
- Do not raise
max_esi_depthto work around a circular include. This defers the failure, increases resource consumption, and does not fix the broken page.
Increase workspace for ESI-heavy pages
ESI processing consumes workspace for each sub-request’s headers and VCL operations. Pages with many includes, large fragment headers, or complex VCL can exhaust the default workspace.
# Check current values
varnishadm param.show workspace_client
varnishadm param.show workspace_backend
Increasing workspace raises per-thread memory consumption. With thread_pool_max threads across thread_pools pools, the total memory cost scales as workspace * max_threads per pool.
Workspace parameter changes require a child restart to take effect. Verify after applying by checking the parameter value with varnishadm param.show workspace_client.
Tune max_esi_depth
If your page layout legitimately requires deep ESI nesting and you are hitting the depth limit on valid (non-circular) includes, increase the limit:
# Check current value
varnishadm param.show max_esi_depth
# Increase (live change, no restart needed)
varnishadm param.set max_esi_depth 10
Tradeoff: Higher depth allows more recursion, which means more sub-requests, more thread time, more workspace consumption, and more delivery latency. Only increase if you have verified the include hierarchy is not circular.
Handle version-specific error behavior
If you upgraded Varnish and ESI behavior changed:
- Varnish 7.3 introduced strict error handling: ESI sub-requests returning non-200/204 would fail. To allow other status codes, enable
param.set feature +esi_include_onerrorand useonerror="continue"in ESI tags. - Varnish 7.5 reverted this: sub-requests are processed regardless of status code. The
esi_include_onerrorfeature flag now enforces 200/204-only behavior, which is the opposite of 7.3. If you had this flag enabled on 7.3 and upgraded to 7.5, behavior flips. - Varnish 6.x and earlier accept all status codes in ESI includes (pre-7.3 behavior).
# Check version and feature flag state
varnishd -V
varnishadm param.show feature
Prevention
- Monitor
MAIN.esi_errorsandesi_warningsindependently. They indicate different problems (failed includes vs. malformed tags) but both produce broken pages. - Add health probes for fragment endpoints. ESI sub-requests go through the normal backend fetch path. If fragment endpoints are sick, pages break.
- Cache fragment responses with grace. Fragment endpoints should have TTL and
beresp.graceset so that transient backend failures do not break page assembly. - Audit include hierarchies for cycles before deploying template changes. Circular includes only surface in production when the specific page template is requested.
- Size workspace for your worst-case ESI page. Count the maximum number of includes per page, estimate header sizes for each fragment, and set
workspace_clientwith headroom. Re-check after any change that adds headers (analytics, tracking, A/B testing). - Be cautious with ESI over HTTPS. Varnish does not support ESI includes over HTTPS and switches to HTTP. If your fragment URLs are HTTPS, this can cause mixed-content issues or failed includes.
How Netdata helps
- Per-second
MAIN.esi_errorsandesi_warningsrates detect ESI failures within seconds of onset. Per-second granularity catches short bursts that minute-level polling misses. - Workspace overflow correlation: Netdata surfaces
ws_client_overflow,client_resp_500, andlosthdralongside ESI error counters. When ESI errors and workspace overflow move together, the diagnosis narrows immediately to workspace sizing rather than backend failures. - Backend health alongside ESI errors: Fragment endpoint failures are the leading cause of ESI errors. Netdata’s per-backend health probes and
fetch_failedcounters sit next to ESI metrics, so you can confirm whether the include failure originated from a sick backend. - Anomaly detection on ESI counter rates: Flags unusual
esi_errorsoresi_warningsspikes even when absolute rates are low, useful for catching gradual increases from template drift or header bloat.
Related guides
- Varnish cache hit ratio dropped: hit rate collapse and backend overload
- Varnish cache stampede: a popular object expires and the herd hits the backend
- Varnish cache_hitpass / cache_hitmiss climbing: uncacheable content bleeding to the backend
- How Varnish actually works in production: a mental model for operators
- Varnish monitoring checklist: the signals every production cache needs
- Varnish monitoring maturity model: from survival to expert
- Varnish sess_dropped vs req_dropped: HTTP/1 connection drops and HTTP/2 stream drops
- Varnish thread pool exhaustion: workers all busy, queue full, sessions dropped
- Varnish thread pool tuning: thread_pool_min, thread_pool_max, and thread_pools
- Varnish thread_queue_len above zero: requests waiting for a worker
- Varnish threads_failed: the OS refusing to create worker threads
- Varnish threads_limited climbing: hitting thread_pool_max






