LIVE render: page template parsed 3x on a page's first render, twice without page context (deterministic 404 on cold pages)
@danielsilva-dotcms is already working on this.
Since Sep 8, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
On the first LIVE render of a page — i.e. whenever the page's compiled Velocity resource is not in cache — dotCMS parses the page's template three times within a single HTTP request instead of once. Two of those parses run in a Velocity context with neither $dotPageContent nor $HTMLPAGE_IDENTIFIER set. Every subsequent render of that page parses once, correctly.
This is deterministic, not intermittent. It reproduces 100% of the time on a cold page on a stock instance with the official demo starter and a single Advanced Template — no plugins, no custom content, no vanity URLs.
Any template logic that depends on page-scoped variables therefore executes against an empty context on a page's first render. Where that logic gates output — as it does on the reporting customer's site, via a macro that calls $response.sendError(404) when a page-scoped lookup comes back empty — the first visitor to any cold page receives a 404, and a refresh then succeeds.
Impact scales with how often pages go cold: cache eviction, publishes, node restarts, deploys. On the reporting customer's 2-pod cloud environment, a sweep of 105 published pages returned 95 × 404 after ~20h idle, decaying to ~8 × 404 once traffic had warmed the site. Because a failed render is never added to the page cache (VelocityLiveMode only calls pageCache.add when the response status is unchanged) while a successful one is cached for the page's TTL, a single successful request masks the fault for the whole TTL window — which is why this presented as intermittent for weeks.
Evidence
- The two extra parses occur inside the same request object (same
HttpServletRequestidentity hash). $HTMLPAGE_IDENTIFIERis empty on both. That value is set in exactly one place —PageRenderUtil.java:225— andPageLoaderemits it into the compiled page template. So those parses are not running inside the compiled page.PageLoader.java:130emits#set($dotPageContent = $dotcontent.find("<inode>"))and only emits#parse('<mode>/<templateInode>.template')afterwards, so the extra parses cannot be an early pass through the compiled page either.javax.servlet.include.request_uriandjavax.servlet.error.request_uriare both empty on the failing parses, so neither is a servlet include nor an error dispatch.- The
$dotcontentviewtool is present and functional throughout, andContentUtils.find()logs nothing — so$dotcontent.find(pageInode)is never called on those parses, rather than failing. x-dotrequest-costcorroborates:@RequestCost(Price.VELOCITY_MERGE)charges per template merge, and cold renders cost several times a warm one.
Not identified: which code path in core issues the two extra parses. Whoever picks this up should start from the reproduction rather than from a theory — five plausible mechanisms were tested and eliminated during triage (the /api/v1/page/render two-pass builder, BlockDirectiveCache output caching, the page-cache TTL / striped-lock release change in 5a3033a42a, #parse vs #dotParse theme inclusion, and the page-cache key / query string).
Steps to Reproduce
Reproduces 100% on a cold page. ~10 minutes end to end.
1. Stand up 26.08.19-04
docker-compose.yml with three services:
db:pgvector/pgvector:pg18opensearch:opensearchproject/opensearch:1,OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m"dotcms:dotcms/dotcms:26.08.19-04,CMS_JAVA_OPTS: '-Xmx1g ',DOT_INITIAL_ADMIN_PASSWORD: 'admin',CUSTOM_STARTER_URL: 'https://repo.dotcms.com/artifactory/libs-release-local/com/dotcms/starter/20260630/starter-20260630.zip'
2. Create an Advanced Template (drawed: false) with this body:
<!doctype html>
<html><head><title>fd38742</title></head><body>
#set($n = $request.getAttribute("parseCount"))
#if(!$n)
#set($n = 0)
#end
#set($n = $n + 1)
#set($ignore = $request.setAttribute("parseCount", $n))
PARSE_MARK n=$!{n} pc=$!{dotPageContent.identifier} hp=$!{HTMLPAGE_IDENTIFIER}
#if(!$dotPageContent)
$response.sendError(404)
#end
</body></html>
curl -u admin@dotcms.com:admin -H 'Content-Type: application/json' \
-X POST http://localhost:8082/api/v1/templates -d @template.json
curl -u admin@dotcms.com:admin -H 'Content-Type: application/json' \
-X PUT http://localhost:8082/api/v1/templates/_publish -d '["<templateId>"]'
3. Create ~50 pages on that template with cachettl: 3600, via PUT /api/v1/workflow/actions/default/fire/PUBLISH, contentType htmlpageasset, urls /fd-repro-1 … /fd-repro-50.
4. Restart the dotCMS container — any cache flush that evicts the compiled page resource will do.
5. Request each page twice:
for i in $(seq 1 50); do
a=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:8082/fd-repro-$i")
b=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:8082/fd-repro-$i")
echo "/fd-repro-$i first=$a second=$b"
done
Observed
first=404 second=200 on every page (12/12 and 50/50 in separate runs).
Remove the #if(!$dotPageContent)$response.sendError(404)#end block and read PARSE_MARK from the response body instead. The parseCount request attribute survives across parses, so the visible n is the total number of times the template was parsed in that request:
| Test | Result |
|---|---|
| First render of each of 50 pages | 49 × n=3 (the 1 × n=1 had been pre-warmed) |
| 12 repeat hits on one warm page | n=1 every time |
| Warm page → container restart → 3 hits | n=1 → n=3 → n=1 → n=1 |
Scope note
Confirmed on an Advanced Template (drawed=false, the #parse('<mode>/<templateInode>.template') branch in PageLoader). A drawn/theme template on the same instance showed a cold render costing ~2.8× a warm one (x-dotrequest-cost 218 vs 79), consistent with the same duplication, but this was not verified with the parse counter — worth confirming, as it determines the blast radius.
Acceptance Criteria
- A page's template resource is parsed exactly once per LIVE page render, including the first render after the compiled page resource is evicted.
- Any Velocity evaluation of a page template is guaranteed to have
$dotPageContentand$HTMLPAGE_IDENTIFIERpopulated, or provably never occurs outside the page-render context. - The reproduction above yields only
n=1on every request, including first renders. - Regression test covering the parse-count-per-request invariant for an anonymous LIVE render on an Advanced Template.
- Confirm or rule out drawn/theme templates (see Scope note).
- Root cause documented — specifically which code path issues the extra parses.
dotCMS Version
Reproduced on 26.08.19-04 (clean local instance, official demo starter, single Advanced Template). Originally reported on 26.08.19-04 on dotCMS Cloud, 2-pod cluster.
Not reproduced on the reporting customer's pre-upgrade 24.12.27_lts_v24 instance, which serves the same content and templates — but that instance was never tested under the cold-cache conditions that make the failure appear, so "introduced by the upgrade" is a reasonable read rather than a confirmed one. Worth checking against the current LTS line, since any customer using Advanced Templates would be exposed.
Severity
High - Major functionality broken
Links
- Freshdesk ticket #38742 — https://dotcms.freshdesk.com/a/tickets/38742
- Possibly related, closed, different code path: #21286
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.