Nginx can remove a surprising amount of load from your application stack, but only when caching rules are deliberate. This guide walks through a maintainable approach to FastCGI cache, proxy cache, and static file caching so you can improve response times without creating a tangle of stale pages, broken sessions, or hard-to-debug cache behavior. The focus is operational: what to cache, what to skip, how to structure configuration, and how to revisit decisions as your app, API, and deployment workflow evolve.
Overview
This section gives you the mental model needed to use Nginx caching confidently. The short version is that Nginx can cache different kinds of responses at different layers, and each layer solves a different problem.
There are three common caching jobs in an Nginx setup:
- FastCGI cache for dynamic pages generated through PHP-FPM or another FastCGI upstream.
- Proxy cache for HTTP upstreams such as app servers, APIs, microservices, and internal backends.
- Static file caching for assets like CSS, JavaScript, fonts, images, and downloaded files.
These are related, but they are not interchangeable. FastCGI cache sits in front of dynamic page generation through FastCGI. Proxy cache stores responses returned by an HTTP backend. Static file caching is usually about headers and browser behavior rather than storing origin responses in an Nginx cache zone.
Teams often run into trouble because they treat all cached content the same. A homepage, a session-bound dashboard, a product image, and a JSON API response all have different freshness requirements and different risks if served stale. A maintainable Nginx caching guide starts with separation: separate content types, separate cache policies, separate bypass rules, and separate ways to inspect results.
It also helps to remember that Nginx caching is only one part of the wider caching path. Browser caches, CDNs, upstream app caches, and validators like ETag or Last-Modified all influence the final behavior. If you are troubleshooting odd refresh behavior in the browser, pair your server-side work with a browser-side inspection workflow; How to Debug Caching Issues in Chrome DevTools is a useful companion.
Core framework
This section outlines a practical framework you can reuse across projects. Instead of starting from directives, start from content categories and risk.
1. Classify responses before writing config
A simple classification model keeps configuration understandable:
- Public and mostly stable: marketing pages, documentation pages, category pages, anonymous API responses.
- Public but fast-changing: inventory-sensitive listings, status endpoints, frequently updated feeds.
- User-specific: dashboards, carts, account pages, personalized APIs.
- Static versioned assets: files with fingerprints or hashes in filenames.
- Static unversioned assets: files that may change without URL changes.
Once you have these categories, the caching rules become easier to reason about. Public and stable content is a candidate for longer TTLs. User-specific content usually needs bypass logic or should not be cached at all. Versioned static assets can usually receive aggressive browser caching because the filename itself acts as a version key.
2. Decide the cache key deliberately
The cache key defines what Nginx considers the same response. A weak cache key creates incorrect hits. A bloated cache key reduces hit rates. In most setups, include the scheme, request method when relevant, host, and request URI. Add selected query parameters only when they affect the response. Be careful with cookies and headers; including too much can fragment the cache, but ignoring important variation can leak content between users.
As a rule of thumb:
- Include query strings for routes where they change the payload.
- Ignore tracking parameters that do not change content.
- Do not cache user-specific content unless variation is explicit and safe.
- Keep cache key logic documented near the location block it affects.
3. Create explicit bypass rules
Most production issues happen not because caching exists, but because bypass rules are incomplete. Common bypass conditions include:
- Authenticated sessions or logged-in cookies
- POST, PUT, PATCH, and DELETE requests
- Admin paths
- Cart, checkout, or account routes
- Preview, debug, or cache-busting parameters
Even if your app emits sensible headers, teams often choose to reinforce those rules in Nginx. The goal is clarity. A future operator should be able to answer, quickly, why a given route was cached or skipped.
4. Separate cache zones by workload
Do not throw everything into one generic cache definition. A cleaner pattern is to create different cache zones for different traffic profiles. For example:
- A small, hot cache zone for HTML pages
- A larger zone for API GET responses
- A browser-cache-focused strategy for static files
This separation helps with tuning storage size, eviction behavior, TTLs, and purge strategy. It also improves debugging because you know which part of the stack is responsible for a cache hit.
5. Use response headers for visibility
Add a simple response header such as X-Cache-Status that exposes whether the request was a HIT, MISS, BYPASS, or EXPIRED. This one addition can save a large amount of time when verifying behavior in browsers, synthetic tests, and load tests.
Similarly, set clear Cache-Control headers for content that browsers or CDNs should cache. If you are deciding between validators, read ETag vs Last-Modified: Which Validator Should You Use? and 304 Not Modified Explained: When Revalidation Helps or Hurts. Nginx caching works best when origin behavior and downstream cache headers agree.
6. Prefer boring configuration over clever configuration
Nginx supports powerful conditional logic, but maintainability matters more than compactness. Use named maps, grouped skip variables, and comments that explain intent. A config that is slightly longer but obvious is safer than a short config that only one person understands.
7. Treat invalidation as part of the design
Before enabling caching, answer the invalidation question: how will stale content be cleared or tolerated? Invalidation options include short TTLs, cache versioning, selective purge mechanisms, deploy-time clears, and content-aware cache busting. If your team cannot invalidate safely, use shorter TTLs and narrower scope until you can.
Practical examples
This section turns the framework into concrete patterns you can adapt. The examples are intentionally conservative; most teams should start here, then tune based on real traffic and failure modes.
FastCGI cache for anonymous HTML
A common use case is caching PHP-generated pages for anonymous users while bypassing logged-in sessions, admin pages, and write requests. The practical pattern looks like this:
- Define a FastCGI cache path and zone for HTML responses.
- Cache only GET and HEAD requests.
- Bypass when session cookies are present.
- Skip cache on admin, cart, account, checkout, and preview paths.
- Use a short TTL to reduce risk, then increase if behavior is stable.
- Expose cache status through a response header.
This pattern works well for CMS-driven sites, documentation portals, or product pages where anonymous traffic dominates. The key tradeoff is freshness versus load reduction. Short TTLs reduce stale risk but create more misses. Longer TTLs improve hit rate but require a stronger invalidation story.
One maintainable approach is to define a skip variable using map statements rather than nesting many if blocks inside a location. That keeps cookie, method, and path logic centralized. When someone adds a new authenticated route later, they update one place instead of hunting through multiple location blocks.
Proxy cache for API and app upstreams
Proxy cache is a better fit when Nginx sits in front of HTTP services such as Node.js apps, Go services, internal APIs, or containerized backends. A careful pattern for read-heavy APIs includes:
- Cache only safe methods like GET and HEAD.
- Use a cache key that includes URI and meaningful query parameters.
- Bypass requests carrying authorization headers unless the API is explicitly public.
- Honor or set conservative
Cache-Controlsemantics. - Use short TTLs for rapidly changing resources.
- Consider stale-serving settings carefully for transient upstream failures.
This is especially useful for endpoints that are expensive to compute but not personalized, such as search suggestions, public resource lists, or metadata endpoints. The tradeoff is correctness around variation. If the response changes based on locale, device, or selected headers, make that variation part of the key or avoid caching.
For APIs behind multiple layers, it is also worth deciding whether Nginx should respect upstream cache headers or override them selectively. In some teams, the app owns freshness entirely. In others, Nginx enforces a stricter edge policy to prevent accidental no-cache behavior. Either model can work; what matters is that ownership is clear.
Static file caching with versioned assets
Static file caching is often the safest performance win. If your build pipeline outputs hashed filenames such as app.4f8c2.js or styles.a91d.css, you can set a long browser cache lifetime because a content change creates a new URL.
A strong baseline for versioned assets includes:
- Long
max-agevalues immutablewhere appropriate- Consistent compression behavior
- Clear content types
- Predictable handling at the CDN layer, if present
For unversioned assets, be more cautious. If the URL stays the same across deploys, clients may hold stale content longer than you expect. In that case, shorter TTLs or validators can be safer. This is one reason many teams treat asset fingerprinting as a deployment requirement rather than an optimization.
If you want a broader production review, HTTP Caching Checklist for Production Sites is a useful next step.
Cache headers for mixed workloads
Many real applications serve a mix of HTML, APIs, and assets from the same Nginx instance. In those cases, it helps to align Nginx cache zones with downstream headers:
- HTML pages: short to moderate server-side cache, cautious browser caching, explicit bypass on session routes.
- Public APIs: short TTLs, explicit variation rules, no caching for auth-bound endpoints unless carefully designed.
- Versioned assets: long browser cache, minimal revalidation overhead.
- Admin and write paths: bypass cache and prioritize correctness.
This approach reduces confusion because each content type has a predictable policy. It also gives your team a common language for incident response: “This is a proxy-cached public API with a 30-second TTL and auth bypass,” for example, is immediately actionable.
Common mistakes
This section highlights failure patterns that cause the most operational pain. Avoiding them matters more than squeezing out a few extra points of hit ratio.
Caching personalized content accidentally
The classic mistake is serving authenticated or user-specific content from a shared cache. This usually happens because cookies, authorization headers, or path exclusions were not accounted for. If a route can vary by user, assume it should bypass cache unless you have a clear and safe variation strategy.
Using one TTL for everything
A single global cache time is easy to set and hard to justify. Static assets, HTML pages, and APIs rarely have the same freshness needs. Broad defaults are acceptable as a starting point, but they should quickly give way to route-based rules.
Ignoring query string behavior
Some teams cache every query-string variation and end up with fragmented caches full of low-value entries. Others strip query strings too aggressively and merge distinct responses. Audit which parameters actually affect content. Treat campaign parameters, preview flags, pagination, filters, and localization differently.
Making invalidation an afterthought
If publish, deploy, or purge workflows are unclear, stale content incidents become routine. Before raising TTLs, document how changes propagate. If invalidation is manual, keep TTLs modest and make the manual process obvious.
Forgetting browser and CDN interaction
Nginx may be working perfectly while the browser or CDN does something different. Server-side cache HITs can coexist with browser revalidation, stale local assets, or an intermediate cache honoring different headers. When behavior looks inconsistent, inspect each layer instead of assuming Nginx is the only actor.
Overusing conditional logic in config
Dense configurations with many nested exceptions become fragile during incidents. Prefer named maps, separate includes for cache policies, and comments that explain why a rule exists. Operational clarity is a feature.
Not instrumenting cache outcomes
If you cannot easily tell whether a request was a HIT, MISS, or BYPASS, tuning becomes guesswork. Add response headers and include cache status in logs where useful. Verification should be quick enough to do during code review, deployment, and incident response.
When to revisit
This final section is meant to be practical. Nginx caching should not be configured once and forgotten. Revisit it whenever the shape of your traffic, application, or deployment process changes.
Review your caching rules when any of the following happens:
- You add authentication or personalization. A page that was safely public may no longer be cacheable.
- You change URL patterns or routing. New paths can bypass old assumptions.
- You introduce a CDN or another reverse proxy. Header ownership and cache layering need to be rechecked.
- You move from monolith to services. FastCGI and proxy cache may need different responsibilities.
- You add asset fingerprinting. Static file caching can become much more aggressive.
- You see cache-related incidents. Stale content, missing updates, or user leakage should trigger a policy review, not just a one-off fix.
- You change deployment cadence. More frequent deploys often require better invalidation or shorter TTLs.
- You adopt new app headers or standards. Cache-Control, validators, and upstream behavior should remain aligned.
A simple recurring checklist helps:
- List routes by content type: HTML, API, asset, admin, authenticated.
- Confirm which ones are cached at Nginx, browser, and CDN layers.
- Verify cache keys and variation inputs.
- Test bypass conditions with real session and auth scenarios.
- Inspect response headers and cache status in staging and production.
- Review invalidation steps during deploys and content updates.
- Document any exceptions near the relevant config blocks.
If you want to make this routine part of operations, tie it to release checklists and post-incident reviews. Cache policy is not only a performance concern; it is also a correctness concern.
As a next step, compare your live behavior against a broader production baseline with HTTP Caching Checklist for Production Sites, and use Chrome DevTools cache debugging guidance when client-side results do not match server expectations.
The main principle to keep is simple: cache aggressively only where correctness is easy to preserve. For everything else, start conservative, measure, and expand with explicit rules. That approach tends to survive team turnover, traffic growth, and architecture changes much better than a clever but brittle setup.