A full-page cache is a server-side cache that stores the complete rendered HTML response of a page and returns that stored copy to later visitors, so the application code, template engine and database queries do not run again for each request. In server performance it is usually the single most effective way to cut response time and raise requests per second for content that many visitors see in the same form.
The cache can live in the web server, in a dedicated reverse proxy such as Varnish, in an application plugin, or at a CDN edge. Wherever it sits, the principle is the same: render once, serve many times until the content changes or the stored copy expires.
How a full-page cache serves requests
Each incoming request is turned into a cache key, normally built from the host, the path, the query string and sometimes selected headers such as the device type or language. If a valid entry exists for that key, the cache returns a hit directly, often in under a millisecond from memory. If not, the request is a miss: it goes to the application, and the response is stored for the next visitor.
Freshness is governed by a time to live (TTL) and by invalidation. A TTL lets an entry expire after a set period, while invalidation, also called purging, removes specific entries the moment content changes. Many systems also support stale-while-revalidate, which serves an expired copy briefly while a fresh one is generated in the background.
The table below summarizes the main parameters that control a full-page cache and their typical ranges.
| Parameter | Typical range | Effect |
|---|---|---|
| TTL for HTML | 1 minute to 24 hours | Longer TTL raises hit ratio but risks stale pages |
| Cache key | Host, path, query, selected headers | More variation means more entries and fewer hits |
| Grace or stale period | Seconds to hours | Keeps serving during regeneration or backend failure |
| Storage size | Hundreds of MB to several GB | Too small causes evictions of popular pages |
| Bypass rules | Cookies, logged-in users, POST | Prevents caching of personal or unsafe responses |
Where the full-page cache can sit
The location of the cache decides how much work a hit avoids and who controls invalidation. A cache closer to the visitor saves more network distance, while a cache inside the application understands content changes best.
| Placement | Example | Strength | Weakness |
|---|---|---|---|
| Application plugin | Cache written to disk by a CMS plugin | Easy setup, aware of content changes | PHP or the app may still start |
| Web server module | Nginx FastCGI cache | Skips the application entirely | Purging needs extra tooling |
| Reverse proxy | Varnish in front of the server | Very fast, flexible rules in VCL | Extra layer to operate |
| CDN edge | Edge cache with HTML rules | Serves from locations near visitors | Purge propagation takes time |
Layers can be combined. A common design uses a CDN edge for anonymous visitors and a local proxy or server cache as a second tier, so a miss at the edge still avoids rendering in most cases.
Full-page cache compared with other caching layers
A full-page cache works at the coarsest granularity: the whole response. Other server-side caching layers store smaller pieces and still require the application to assemble the page, which makes them complementary rather than competing.
| Layer | What it stores | Work still done on a hit |
|---|---|---|
| Full-page cache | Entire HTML response | Almost none |
| Object cache | Query results and computed objects | Template rendering and application logic |
| OPcache | Compiled PHP bytecode | Executing code and running queries |
| Browser cache | Responses on the visitor device | None, but only for that visitor |
Because an object cache and OPcache speed up misses and uncacheable requests, they remain valuable even with a strong full-page cache. Logged-in dashboards, carts and search results often bypass the page cache and depend on those lower layers and on the size of the PHP worker pool.
Configuring the cache for dynamic sites
The hardest part of full-page caching is deciding what must never be cached. Any response that contains personal data, a session-specific token or a cart total must bypass the cache or be split so that the personal part loads separately.
| Request type | Recommended handling | Reason |
|---|---|---|
| Anonymous article or product page | Cache with long TTL and purge on update | Same for every visitor |
| Home and category pages | Cache with shorter TTL | Change often as content is added |
| Logged-in pages | Bypass by session cookie | Contain personal data |
| Cart, checkout, account | Never cache | Per-user state and security tokens |
| Pages with small personal blocks | Cache shell, load blocks via JavaScript or ESI | Keeps most of the page cacheable |
Cache warming after a deployment or a full purge prevents the first visitors from all hitting a cold cache at once. Request coalescing, where the cache sends only one request to the backend per missing key, protects the server from a cache stampede on popular pages.
Pitfalls and failure modes
- Leaking personal content: caching a page rendered for a logged-in user and serving it to everyone is the most serious failure.
- Cookie noise: analytics or consent cookies that vary per visitor can make every request a miss if the cache treats all cookies as significant.
- Query string variation: tracking parameters create thousands of keys for the same page unless they are stripped or ignored.
- Stale content after edits when purging is not tied to publishing events.
- False confidence in benchmarks: a test that hits only cached URLs hides how slow uncached pages remain.
A useful health metric is the hit ratio for HTML. Content sites commonly reach high ratios for anonymous traffic, while shops with many personalized pages see much lower values; tracking the trend matters more than any single target.
A non-technical view
Building a web page takes work. The server reads the database, fills in a template and assembles the result. A full-page cache keeps a finished copy of the page after the first visit. The next visitors get that copy right away.
It is like a bakery that bakes a tray of bread in the morning instead of baking one loaf per customer. Most people get fresh bread instantly. Special orders, like a cake with your name on it, are still made individually.
A concrete example
A regional news site on a single server handles about 40 requests per second on normal days, with each article taking around 400 milliseconds to render. When a story goes viral, traffic jumps to several hundred requests per second, the PHP worker pool fills up and visitors start seeing timeouts.
The team adds an Nginx FastCGI cache for anonymous visitors, with a five-minute TTL, a bypass for the login cookie and a purge triggered whenever an editor updates an article. During the next traffic spike, the HTML hit ratio stays high, cached articles are served in a few milliseconds, and the backend load barely changes because almost every request is answered from the cache.