Cache Keys and Query Strings on CDNs
A static site should have an almost perfect CDN cache hit ratio: every page is a file that changes only on deploy. In practice, many static sites sit well below 90%, and the reason is often not the cache headers but the cache key. CDNs cache by URL including the query string, so /pricing/, /pricing/?utm_source=newsletter, /pricing/?fbclid=IwAR2… and /pricing/?utm_source=linkedin&utm_medium=social are four different cache entries for one file. Every campaign, every social share with a click identifier, every email link with tracking parameters creates variants that miss the cache and travel to the origin.
This guide measures how much query-string fragmentation costs a static marketing site, then normalizes the cache key on Cloudflare and on CloudFront so tracking parameters are ignored while parameters that matter are kept. It is part of CDN Caching Rules for SSGs. The site is a 420-page marketing site and blog with heavy campaign traffic, self-hosted on S3 behind CloudFront, with a Cloudflare-fronted documentation site used for comparison.
Prerequisites
- CDN logs or analytics with cache status per request (Cloudflare Analytics, CloudFront standard logs or real-time logs).
- A list of query parameters the site actually uses — search pages, pagination, filters — which must stay in the cache key.
- Access to the CDN's cache key configuration (Cloudflare Cache Rules, CloudFront cache policies).
Step 1: Measure the Fragmentation
Group a week of CDN logs by path and count distinct query strings and cache status. With CloudFront standard logs in Athena:
SELECT cs_uri_stem AS path,
count(DISTINCT cs_uri_query) AS variants,
sum(CASE WHEN x_edge_result_type IN ('Hit','RefreshHit') THEN 1 ELSE 0 END) * 1.0 / count(*) AS hit_ratio,
count(*) AS requests
FROM cloudfront_logs
WHERE date BETWEEN date '2026-09-01' AND date '2026-09-07' AND cs_uri_stem LIKE '%/'
GROUP BY cs_uri_stem ORDER BY requests DESC LIMIT 20;
The home page had 3,400 distinct query strings in a week and an edge hit ratio of 61%. Across all HTML, 34% of requests carried a query string, and 88% of those carried only tracking parameters. The overall HTML hit ratio was 68%.
| Parameter | Share of HTML requests carrying it |
|---|---|
utm_* | 21% |
fbclid | 6% |
gclid / gbraid / wbraid | 4% |
mc_cid / mc_eid (Mailchimp) | 2% |
ref, source | 1% |
| anything else | 4% |
Step 2: Decide What Belongs in the Key
The cache key should include exactly what changes the response. For a static site built with a generator, the HTML never varies by query string — with two common exceptions: a search results page that reads ?q= in JavaScript (the HTML is the same shell, so it can still be cached once) and assets cache-busted with ?v= (which must stay in the key). On this site:
- Keep:
v(legacy asset versioning on a few files). - Exclude: everything else for HTML. The search page's
?q=is read client-side, so the shell can be shared.
An allowlist ("include only v") is safer than a denylist of tracking parameters, because new trackers appear every year. The denylist approach is still useful where the site legitimately uses many parameters.
Step 3a: Cloudflare Cache Rules
On Cloudflare, a Cache Rule sets the cache key for matching requests. For HTML and assets on the documentation site:
Rule: "Static site cache key"
When: hostname eq "docs.example.com"
Then: Eligible for cache
Cache key → Query string → Include only: v
Edge TTL → respect origin
In Terraform, the same rule is a cloudflare_ruleset in the http_request_cache_settings phase with cache_key.custom_key.query_string.include = ["v"]. Cloudflare's "Ignore query string" option on older zones does the same for all parameters, but it also drops v, which caused stale assets on the documentation site during testing.
Step 3b: CloudFront Cache Policies
On CloudFront, the cache key is defined by the cache policy attached to each behaviour. For the HTML behaviour, a custom policy with an allowlist:
{
"Name": "static-html-allowlist",
"DefaultTTL": 0, "MaxTTL": 31536000, "MinTTL": 0,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingBrotli": true, "EnableAcceptEncodingGzip": true,
"QueryStringsConfig": { "QueryStringBehavior": "whitelist", "QueryStrings": { "Quantity": 1, "Items": ["v"] } },
"HeadersConfig": { "HeaderBehavior": "none" },
"CookiesConfig": { "CookieBehavior": "none" }
}
}
Query strings not in the list are neither part of the key nor forwarded to the origin, which for an S3 origin makes no difference because S3 ignores them. Cookies are excluded too — a surprisingly common source of fragmentation when a CDN is configured to forward all cookies and the site has any analytics cookie.
Step 4: Check Nothing Depended on the Parameters
Before rolling out, search the built output and client scripts for code that reads query strings (URLSearchParams, location.search) and for server-side logic in edge functions that varies by parameter. On the marketing site, a Worker-free setup meant nothing varied; on the documentation site, a legacy "print mode" read ?print=1 in a script — client-side, so harmless to cache. If an edge function does vary output by a parameter, include that parameter in the key for the paths it handles.
Step 5: Watch Hit Ratio and Origin Load
After the change, compare a week of logs. Cache hit ratio is the primary metric; origin requests and origin egress are the cost metrics; TTFB at p75 is what readers feel. Alerting on Cache Hit Ratio Drops describes an alert that would have caught the original fragmentation early.
Other Things That Fragment the Key
Query strings are the most common fragmenter, but not the only one. A cache key that includes the Accept-Language or User-Agent header — sometimes added to "support" device-specific pages that no longer exist — splits every URL into hundreds of variants. Cookies in the key are worse. Case differences (/Pricing/ versus /pricing/) and trailing-slash variants create separate entries too, which is one more reason to redirect non-canonical URL shapes at the edge, as in Canonical URLs and Duplicate Content on Static Sites. When a hit ratio looks lower than it should, list the distinct cache keys for the most requested path; the extra dimension usually shows itself within the first few rows.
Measured Impact
| Measure | Full query string in key | Allowlist key |
|---|---|---|
| HTML edge hit ratio | 68% | 96% |
| Home page hit ratio | 61% | 98% |
| Origin requests per day (HTML) | 212,000 | 27,000 |
| HTML TTFB p75 (RUM) | 190 ms | 48 ms |
| LCP p75 on campaign landing traffic | 2.3 s | 1.8 s |
| Distinct cache entries for /pricing/ per week | 1,900 | 1 |
The LCP improvement came almost entirely from campaign traffic, which previously hit uncached variants with an origin round trip to S3 in another region.
Pitfalls & Rollback
- Ignoring all parameters when some matter. Asset versioning with
?v=or edge functions that vary by parameter break; use an allowlist. - Forwarding cookies. Any cookie in the key fragments the cache as badly as query strings.
- Different policies per behaviour. Apply the policy to HTML and asset behaviours deliberately; defaults differ.
- Analytics expectations. Server-side log analytics that relied on parameters at the origin will see fewer of them; client-side analytics is unaffected.
- Rollback: switch the behaviour back to the previous cache policy or disable the Cache Rule; the change applies within minutes.
Conclusion
On a static site, the query string almost never changes the response, so it should almost never be in the CDN cache key. Measure fragmentation from logs, allowlist the few parameters that matter, exclude cookies, and verify nothing reads the parameters server-side. On this site that took the HTML hit ratio from 68% to 96% and cut TTFB for campaign traffic by three quarters.
FAQ
What is a CDN cache key?
The set of request attributes a CDN uses to decide whether two requests can share a cached response. By default it usually includes the scheme, host, path and full query string. Two URLs that differ only in a query parameter are therefore cached separately.
Why do query strings hurt caching on static sites?
Static pages do not change based on marketing parameters like utm_source or fbclid, but each unique combination creates a separate cache entry. Links shared on social networks and in campaigns can create thousands of variants of one page, most of which miss the cache and go to the origin.
Should I ignore the query string entirely?
Only if no URL on the site depends on it. Many static sites use query strings for search result pages or cache-busted assets, so the safer approach is to exclude a list of known tracking parameters, or include only an allowlist of parameters that matter.
Does removing parameters from the cache key break analytics?
No. The parameters are still present in the browser's URL, so client-side analytics reads them as before. Only the CDN's cache lookup ignores them.
Related
- Parent: CDN Caching Rules for SSGs — the full caching setup.
- Tiered Caching and Origin Shield — reducing misses that reach the origin.
- Setting Cache-Control Headers on Cloudflare Pages — the TTL side of caching.
- Deploying a Static Site to S3 and CloudFront — the CloudFront setup.
- Improving TTFB for Static HTML — what better hit ratios do for TTFB.
Filed underHow-tosCloudflareAWS (S3 and CloudFront)