Skip to content

Speed

CDN basics: what a CDN fixes and what it does not

A CDN puts copies of your files near your visitors and takes TLS, HTTP/2 and compression off your server. What that changes, what it cannot, and how to set one up without caching the wrong pages.

getReport teamUpdated 25 Sept 202612 min read

A content delivery network is a set of servers around the world that keep copies of your files and answer visitors from the nearest one. It is the single most recommended speed fix on the web, and it is often recommended for problems it cannot touch. A CDN makes a static file arrive from 20 ms away instead of 150 ms away; it does not make your PHP faster, your database smaller or your JavaScript lighter. This guide separates the two lists, shows how to tell from a report whether a CDN is in front of a site and doing its job, and covers the settings that decide whether it helps or breaks things.

Quick answer

A CDN fixes:

  • Distance: static files (images, CSS, JS, fonts) come from an edge near the visitor, cutting each round trip from 100–300 ms to 10–40 ms.
  • Connection setup: TLS termination, HTTP/2 and HTTP/3 at the edge, regardless of what the origin supports.
  • Compression and, on most CDNs, image resizing and format conversion.
  • Traffic spikes and origin load: cached requests never reach your server.
  • With page caching at the edge: TTFB for the HTML itself, from seconds to tens of milliseconds.

A CDN does not fix:

  • An uncached dynamic page (PHP, a database query, a slow API) unless you cache the HTML at the edge and the page is cacheable.
  • Render-blocking CSS and JavaScript, a 2 MB hero image, a slow third-party script. The bytes arrive faster and still cost the same.
  • Layout shifts, long tasks, anything that happens inside the browser.

Check with the HTTP/2 test: the protocol table and the CDN finding say what is in front of the site and what it is doing.

Why a CDN matters, and why it is not magic

Every request between a visitor and a server pays for distance. A round trip from Zagreb to a server in Frankfurt is about 30 ms; to a server in Virginia, 120–150 ms. A first visit needs a DNS lookup, a TCP handshake, a TLS handshake and the request before the first byte arrives: three or four round trips, so 100 ms against 500 ms before the server has done any work. Then the page references 40 files, each of which needs at least one more round trip. A CDN moves the endpoint of all of those trips to a city near the visitor.

What the edge cannot do is invent content it does not have. When a request arrives for a file the edge has not cached, or for a page marked as uncacheable, it forwards the request to your origin server, waits, and relays the answer. That trip is as slow as before plus a few milliseconds. For a WordPress page without page caching, the CDN adds nothing to TTFB: the edge asks your server, your server runs PHP for 900 ms, the edge relays 900 ms later.

This is why the same CDN produces two very different results. For a static site or a cached CMS, the whole page comes from the edge and TTFB drops to 20–50 ms everywhere in the world. For an uncached page, only the images and scripts benefit, and the number that dominates (TTFB, then LCP) barely moves.

How getReport checks it

The test does a TLS handshake with the site to see which protocol it negotiates, reads the Alt-Svc header for HTTP/3, then loads the page in Chromium and records every response: its protocol, its headers and where it came from. A CDN is detected from response headers that CDNs add: Cloudflare's cf-ray and cf-cache-status (or server: cloudflare), Fastly's x-served-by and x-fastly-request-id, Akamai's x-akamai-* headers, CloudFront's x-amz-cf-id and via. A CDN that adds none of those (some smaller ones, and a host's own CDN behind a plain hostname) can read as "no CDN" even though it works; the protocol table below still shows its effect.

The protocol details table from the HTTP/2 test: HTTP/2 (ALPN) negotiated h2, HTTP/3 advertised with the Alt-Svc header value, the TLS version and cipher, and chips counting the responses per protocol from the Chromium run, with the CDN finding beneath
A site behind a CDN typically shows h2 negotiated, h3 advertised and TLS 1.3 here whatever the origin runs; the CDN finding names the provider it recognised.

The TTFB test is the other half: it measures the first byte from our test location (Frankfurt) with a fresh connection and lists redirect hops. One location is one point on the map. A site whose origin is in Frankfurt scores well here with or without a CDN; the CDN's value shows for the visitors who are not in Frankfurt, which is why the field TTFB (real Chrome users, in the speed panel) is the better judge of whether the edge is helping.

Step by step

1. Decide what you need the CDN for

Three different jobs, in increasing order of setup:

  1. Static files only. Images, CSS, JS and fonts from the edge; HTML still from the origin. Every CDN does this out of the box. It helps LCP (the hero arrives faster) and everything after the first byte, but not TTFB.
  2. Whole site with edge page caching. The HTML is cached too, for anonymous visitors. TTFB drops to the edge's response time. Needs rules to exclude the pages that differ per visitor.
  3. Extras: image optimisation (resize, WebP/AVIF, compression on the fly), a web application firewall, bot rules, Early Hints. Worth having, secondary to the first two.

If your TTFB is over 800 ms on cached pages, job 2 is the one that matters. If TTFB is fine and LCP is not, job 1 plus image optimisation is enough.

2. Put the site behind the CDN

Cloudflare (free plan): add the site, change the domain's nameservers to the two Cloudflare gives you, and set SSL/TLS to "Full (strict)" so the connection to your origin stays encrypted with a valid certificate. Static files are cached by extension by default; HTML is not. HTTP/2, HTTP/3 and Brotli are on.

Bunny, KeyCDN and similar pull CDNs: create a "pull zone" pointing at your origin, get a hostname like cdn.example-shop.hr, and make the site reference its static files from that hostname. WordPress plugins (the CDN's own, or the "CDN" tab in WP Rocket and LiteSpeed Cache) rewrite the URLs for you. HTML still comes from the origin, so this is job 1 only unless you put the whole domain behind it.

Host-integrated CDNs (managed WordPress hosts, Netlify, Vercel, Shopify): already in front of everything, including the HTML on static hosts. Nothing to set up; check the headers to confirm.

Fastly, Akamai, CloudFront: the same idea with more configuration and pricing meant for larger sites. If you are reading a basics guide, start with Cloudflare or Bunny.

3. Cache the HTML at the edge, with bypass rules

Edge page caching is the step that moves TTFB, and the one that goes wrong. The rule is: cache pages that are the same for everyone, bypass pages that are not.

On Cloudflare, Rules → Cache Rules. First the bypass rule, then the cache-everything rule; the first matching rule wins, so order matters:

Text
Rule 1 — Bypass (evaluated first)
  When:  URI Path starts with "/wp-admin"
     or  URI Path starts with "/cart"
     or  URI Path starts with "/checkout"
     or  URI Path starts with "/my-account"
     or  Cookie contains "wordpress_logged_in"
     or  Cookie contains "woocommerce_items_in_cart"
     or  Cookie contains "wp-postpass"
  Then:  Cache eligibility: Bypass cache

Rule 2 — Cache everything
  When:  Hostname equals "example-shop.hr"
  Then:  Cache eligibility: Eligible for cache
         Edge TTL: 2 hours (or "Use cache-control header if present")

The cookie conditions are the important ones: a logged-in editor or a shopper with something in the cart must always reach the origin, or they will see a cached page meant for anonymous visitors. Cloudflare's paid APO add-on for WordPress does this automatically and purges on publish; the rules above are the manual equivalent.

Then tell the edge how long to keep things. Your origin's Cache-Control header is the cleanest control: s-maxage applies to shared caches like a CDN, max-age to browsers:

nginx
# nginx: cache HTML at the edge for an hour, in browsers not at all;
# static files for a year everywhere
location / {
    add_header Cache-Control "public, max-age=0, s-maxage=3600" always;
    # … proxy_pass or fastcgi_pass
}
location ~* \.(css|js|woff2|png|jpg|jpeg|webp|avif|svg|ico)$ {
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

Cache-Control for humans explains every directive.

4. Purge on deploy and on publish

An edge that keeps HTML for two hours shows the old article for two hours after you publish, unless something tells it to forget. WordPress plugins for Cloudflare, LiteSpeed Cache and WP Rocket purge the changed URLs automatically when a post is saved. For a static site, purge after each deploy; for Cloudflare that is one API call, safe to run from CI:

Shell
# Purge everything on a zone after a deploy (Cloudflare API)
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything":true}'

Versioned file names (app.3f9a1c.css) make purging static files unnecessary: the new HTML references a new URL that was never cached.

5. Let the edge handle TLS and protocols

With the CDN terminating connections, visitors get TLS 1.3, HTTP/2 and (on Cloudflare, one toggle under Network) HTTP/3, even if the origin only speaks HTTP/1.1. Keep the origin connection encrypted ("Full (strict)" on Cloudflare; a valid certificate on the origin, Let's Encrypt or Cloudflare's origin certificate). The http2 finding then shows every response over h2 or h3; any http/1.1 responses left are third-party hosts outside your CDN. HTTP/2 and HTTP/3 covers the origin side.

6. Turn on image optimisation if the CDN has it

Cloudflare's Polish (paid plans) and Bunny's Optimizer convert and compress images at the edge; Cloudflare Images and Bunny's on-the-fly resizing serve the size the page asks for. This is job 3, and it is the one that changes LCP: a 1.4 MB JPEG hero served as a 90 KB WebP is faster from anywhere. Without it, resize and convert at the source; see Image sizes that do not hurt.

Platform notes

WordPress

Page caching on the server (LiteSpeed Cache, WP Rocket, the host's cache) and page caching at the edge are two different layers, and you want both: the edge serves most visitors, the server cache makes the origin fast for the misses and for the edges that have not seen the page yet. Keep one plugin in charge of purging. If the site uses a "CDN URL" rewrite for static files, exclude wp-admin and wp-login.php from the rewrite.

Shopify, Wix, Squarespace

Already behind a CDN with edge page caching; there is nothing to install and the TTFB you get is the platform's. The findings still show whether third-party scripts and app-hosted images come over HTTP/2.

Static sites and custom apps

Static hosts are a CDN by definition. For an app with server-rendered pages, the useful pattern is Cache-Control: s-maxage=60, stale-while-revalidate=600 on pages that can be a minute old: the edge serves them instantly and refreshes in the background.

Verify

  • The cdn-detected finding names the provider, or if it does not, the http2 finding shows h2 or h3 for the document and the TTFB test shows a first byte under 100 ms on a page you know is dynamic (proof the HTML came from the edge).
  • The response headers show a cache hit on the second request: cf-cache-status: HIT, x-cache: HIT or the provider's equivalent. curl -sI https://example-shop.hr/ | grep -i -E "cf-cache-status|x-cache|age:" twice in a row; the second should be a HIT.
  • The cache-headers finding passes: static files carry a positive max-age.
  • Logged in, or with an item in the cart, the same header reads BYPASS or DYNAMIC and the page shows your own content.
  • The field TTFB in the speed panel (real visitors, 28 days) moves under 800 ms over the next month; the Frankfurt number alone cannot prove the edge is helping elsewhere.

Common mistakes

  • Caching personalised HTML. Symptom: a customer sees someone else's name or cart. Fix: bypass rules on the login, cart and session cookies, before any cache-everything rule.
  • Expecting the CDN to fix an uncached page. Symptom: TTFB unchanged after "adding Cloudflare". Static files are cached by default; the HTML is not until you add a cache rule, and an uncacheable page stays slow.
  • Double compression or double optimisation. Symptom: garbled files, or two minifiers rewriting the same CSS. Let one layer compress (the edge is fine) and turn the origin's or the plugin's version off.
  • Forgetting to purge. Symptom: the old price shows for two hours after an update. Automatic purge on publish, or short edge TTLs for HTML with long ones for versioned files.
  • "Flexible" SSL. Symptom: the padlock shows but the edge talks to the origin over plain HTTP. Use Full (strict) with a certificate on the origin.
  • Judging the CDN by one test location. Symptom: no change in the Frankfurt TTFB for a Frankfurt origin. Look at the field TTFB and at the responses-by-protocol chips, which show the edge is serving even when the distance gain is zero for that one location.
Check your site before and after Check