Deployed a WPCode PHP filter to fix HFE wpautop injection. curl verification with Cache-Control: no-cache still returned stale cached HTML. Fix appeared not to work — it had worked. LiteSpeed had already cached the old output.
Impact
Live verification of a deployed PHP filter (WPCode snippet) returned stale output. The fix was live but invisible behind LiteSpeed's server-side cache. Caused a false-negative verification result and unnecessary debugging cycle.
Root Cause
LiteSpeed Cache operates server-side and caches full HTML responses. Client-sent HTTP headers — including Cache-Control: no-cache and Pragma: no-cache — are forwarded to LiteSpeed but LiteSpeed ignores them for cache bypass decisions. These headers only control browser cache behavior. The WPCode snippet was active and working; the cached page had not been purged.
Resolution
WordPress Admin → LiteSpeed Cache → Purge All. Immediately served fresh PHP-rendered HTML with the filter applied. curl re-verification confirmed 0 instances of the wpautop injection pattern.
Prevention Pattern
After any WordPress PHP filter or hook deployment: always run LiteSpeed → Purge All before verification. Never use curl with Cache-Control: no-cache to verify filter output on LiteSpeed-cached sites — the header does nothing for server-side cache.
Recovery
Quick fix (minutes)
Deploy risk
low
Detectable
~8 minutes — after curl verification returned unexpected stale output
Repeat risk
medium
Prevention patterns
Ecosystem impact
Resolution Steps
Was verifying a WPCode snippet deployment that fixes a </p><p> injection inside <a> tags caused by WordPress wpautop() running on HFE (Header Footer & Blocks) post-info widgets. The snippet hooks into elementor/widget/render_content and strips the injected paragraph tags from affected widget output.
Ran the verification command after the snippet was activated in WPCode and confirmed "Active":
curl -sL "https://asquaresolution.com/[post-slug]/" \
-H "Cache-Control: no-cache" \
-H "Pragma: no-cache" \
| grep -c '"></p><p>'
Result: 5 — five instances still present.
The snippet was correctly activated. The filter was correctly targeting hfe-post-info content. But the output was coming from LiteSpeed's full-page HTML cache, not from a fresh PHP render.
LiteSpeed Cache is a server-side full-page cache. When a request arrives:
Client HTTP headers like Cache-Control: no-cache and Pragma: no-cache are forwarded to the origin server, but LiteSpeed does not honour them for cache bypass. These headers exist to control browser caches and proxy intermediaries that respect them. LiteSpeed's server-side cache has its own invalidation logic, separate from client-driven cache control.
This is standard behaviour for server-side full-page caches (Varnish, NGINX FastCGI cache, and WP Super Cache behave similarly). The distinction is:
| Cache type | Client no-cache | Bypassed? |
|---|---|---|
| Browser cache | ✓ | Yes |
| CDN (respecting headers) | ✓ | Sometimes |
| LiteSpeed server-side | ✓ | No |
The cached HTML on disk was from before the snippet was activated. LiteSpeed had no reason to invalidate it — no content had been published, no manual purge had been triggered.
The verification result was:
curl count: 5 (still injected)
Interpretation: "The snippet is not working."
Correct interpretation: "The snippet is active and correct, but we are verifying the cached HTML, not live PHP output."
This distinction costs debugging time because the natural next step is to check snippet syntax, hook priority, and filter logic — none of which were wrong.
| Time | Event |
|---|---|
| T+0 | WPCode snippet activated ("Active" status confirmed in admin) |
| T+2 | curl verification run with no-cache headers |
| T+2 | Result: 5 instances — same as before deployment |
| T+5 | Checked snippet code again — syntax correct, hook registered |
| T+8 | Hypothesis: LiteSpeed cache serving stale HTML |
| T+10 | WordPress Admin → LiteSpeed Cache → Purge All |
| T+12 | curl re-verification run |
| T+12 | Result: 0 instances — fix confirmed |
| T+15 | Root cause documented |
For any PHP filter or hook deployed via WPCode on a LiteSpeed-cached WordPress site:
1. Activate snippet in WPCode → confirm "Active" status
2. WordPress Admin → LiteSpeed Cache plugin → Dashboard → Purge All
3. Wait 10 seconds (LiteSpeed processes the purge asynchronously)
4. Verify with curl:
curl -sL "https://[domain]/[slug]/" | grep -c '[pattern]'
Expected: 0 (pattern absent)
Do NOT use -H "Cache-Control: no-cache" in the curl command — it has no effect on LiteSpeed's server-side cache and creates a false impression of bypassing it.
Any code that affects output at the PHP render layer — filters, actions, shortcodes, WPCode snippets, theme function changes — is invisible to any visitor until the cache is purged. This includes:
wp_head)The standard operating procedure after any WordPress code change should include a LiteSpeed Purge All, not just after content publishing. In high-traffic environments, a partial purge (by URL) is preferable to avoid cache warming overhead — but for development and verification, full purge is the safe path.
⚠Verification on LiteSpeed requires a prior Purge All
Verifying any PHP-level change on a LiteSpeed-cached WordPress site without first running Purge All will return stale cached output. The verification result will be meaningless regardless of what HTTP cache headers you send in the request.
Before verifying any WordPress PHP change on LiteSpeed:
Fix Confidence
Recovery Complexity
Pattern Family
This failure belongs to a named recurring pattern. Other failures in this family share the same root cause structure — understanding the pattern prevents multiple failure types simultaneously.
Related Failures in Same Pattern
Prevention Lessons
Completing these lessons would have prevented this failure.
Demonstrated In
This failure occurred in a real production context. These case studies show the full arc from incident to resolution.