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%.

ParameterShare of HTML requests carrying it
utm_*21%
fbclid6%
gclid / gbraid / wbraid4%
mc_cid / mc_eid (Mailchimp)2%
ref, source1%
anything else4%
Share of HTML requests carrying each tracking parameter Horizontal bars of the share of HTML requests carrying each parameter over one week. utm parameters 21 percent. fbclid 6 percent. Google click identifiers 4 percent. Mailchimp identifiers 2 percent. ref or source 1 percent. Other parameters 4 percent. HTML requests by query parameter utm_* 21% fbclid 6% gclid, gbraid, wbraid 4% mc_cid, mc_eid 2% ref, source 1% Other 4%
From one week of CloudFront logs; requests can carry several parameters, so shares overlap.

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.

Cache key normalization for a request with tracking parameters A request for /pricing/ with utm_source, utm_medium and fbclid arrives at the edge. The cache policy removes every parameter not on the allowlist, leaving the key as the host and path only. The lookup hits the single cached copy of /pricing/. The browser keeps the full URL, so analytics still reads the parameters. One cache entry for every campaign variant Request /pricing/?utm_ source=x&fbclid=y Cache policy allowlist v only Key host + /pricing/ Edge hit one shared copy Browser URL parameters intact
The parameters vanish from the cache lookup, not from the page URL.

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.

Hit ratio and TTFB before and after cache key normalization Paired bars. HTML edge hit ratio rose from 68 percent to 96 percent. Home page hit ratio rose from 61 percent to 98 percent.. HTML caching before and after normalizing the key Full query string in key Allowlist key HTML edge hit ratio 68% 96% Home page hit ratio 61% 98%
One week before and one week after on CloudFront, similar traffic volume and campaign mix.

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

MeasureFull query string in keyAllowlist key
HTML edge hit ratio68%96%
Home page hit ratio61%98%
Origin requests per day (HTML)212,00027,000
HTML TTFB p75 (RUM)190 ms48 ms
LCP p75 on campaign landing traffic2.3 s1.8 s
Distinct cache entries for /pricing/ per week1,9001

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.

Filed underHow-tosCloudflareAWS (S3 and CloudFront)