A CDN moves static assets out of your origin server's hot path and serves them from an edge node close to the visitor. For WordPress that means images in wp-content/uploads, theme CSS, plugin JavaScript, and webfonts get delivered from a network of points of presence instead of bouncing across half the planet to your data center. The setup is short and the result is real, but the wrong choice of CDN type breaks more sites than it speeds up. Before you create a pull zone, you need to know which kind of CDN you actually need.
What a CDN does for WordPress performance
A CDN is a geographically distributed network of edge servers that sits between your visitors and your origin. When the visitor requests a static file, Google's web.dev guide on content delivery networks explains the mechanism in three parts: the CDN terminates the TCP connection close to the visitor (eliminating long-distance setup costs), it serves the file from the edge cache when it has one, and it maintains pre-warmed persistent connections to the origin for cache misses. Some CDNs also route traffic over their own optimized backbone networks instead of public BGP, which shaves additional latency. A 90% cache hit ratio is the target you are aiming for. Recommended features in a modern CDN include Brotli compression, TLS 1.3, and HTTP/2 or HTTP/3.
For WordPress specifically, the static assets are the prize. Images make up roughly half the byte payload of a typical web page, and on a media-heavy WordPress site that ratio is even higher. Offload those bytes to the edge and your origin spends its time on PHP and the database, not on serving JPEGs.
There is one important caveat that catches every first-time CDN user. WordPress HTML is dynamic, not static. A page request runs PHP, queries MySQL, and constructs the response on the fly. A standard asset CDN does not cache that HTML. The visitor still waits for your origin to render the page, then images and CSS load fast from the edge. If your TTFB is the bottleneck, an asset CDN will not fix it on its own. You need a separate page-cache layer for HTML, either at the WordPress level (a caching plugin) or at the CDN level (Cloudflare APO, full-page edge caching). For the broader picture of where TTFB time goes on a WordPress request, see what TTFB is and why WordPress sites often have a high one.
Full proxy CDN vs static asset (pull-zone) CDN
This is the choice nobody warns you about and the one that decides everything else.
A pull-zone CDN is the traditional model. You sign up for a CDN account (BunnyCDN, KeyCDN, CloudFront), create a pull zone pointing at your origin URL, and the CDN gives you a hostname like example.b-cdn.net. You typically front it with your own subdomain such as cdn.yoursite.nl via a CNAME. A WordPress plugin then rewrites every asset URL on every page to point at that CDN hostname instead of your origin. Only static assets are cached at the edge, and they live on a different domain from your HTML. The pull zone fetches assets from your origin on the first request and caches them for subsequent requests.
A full proxy CDN like Cloudflare works completely differently. When you enable Cloudflare's orange-cloud proxy, all traffic to your domain (HTML and assets together) routes through Cloudflare's edge network. You do not get a separate cdn.yoursite.nl hostname; your existing yoursite.nl is the CDN. Cloudflare automatically caches static files by file extension on its free plan and leaves HTML uncached unless you turn on Cloudflare APO ($5 per month for Free-plan users) or write Cache Rules manually.
These two architectures do not mix cleanly. If you are already on Cloudflare with the orange cloud enabled, do not also set up a pull-zone CDN with URL rewriting. Cloudflare is already caching your static assets at the edge under your own domain. Adding CDN Enabler with a cdn.yoursite.nl hostname means assets get rewritten to a hostname Cloudflare does not own, which sends them outside Cloudflare's cache and defeats most of the point. Pick one model, not both. For the setup steps and pitfalls of the Cloudflare path specifically, see WordPress with Cloudflare: the correct configuration.
The decision is simple in practice. If you are already on Cloudflare and the free plan is acceptable, you have a CDN already and the rest of this article is about the alternative path. If you are not on Cloudflare and you want a separate, pure asset CDN with usage-based billing and a familiar pull-zone model, read on.
A short word on push zones, since people ask: a push zone is a model where you upload files yourself to a storage bucket and the CDN distributes them from there. It is appropriate for large, rarely-changing files like videos and software downloads, but it is awkward to manage with WordPress media because uploads are dynamic. For a typical WordPress site, pull zones are the standard and what the rest of this article covers.
Setting up BunnyCDN as a pull-zone CDN
BunnyCDN is the popular pull-zone CDN among WordPress users. Pricing is pay-per-GB with no monthly minimum on pull zones, the dashboard is straightforward, and there is an official WordPress plugin in addition to the generic CDN Enabler plugin.
The setup, in order:
- Create a Bunny.net account and add a Pull Zone from the dashboard. The Bunny Pull Zone API documents the POST /pullzone endpoint for programmatic creation, but the dashboard is the easier path the first time. Set the Origin URL to your WordPress site (
https://yoursite.nl). The Name field becomes the subdomain onb-cdn.net. - Take note of the assigned CDN hostname, which will look like
yoursite.b-cdn.net. This is what URL rewriting will point at if you stop here. - Add a custom CNAME (optional but recommended). In your DNS, create
cdn.yoursite.nl CNAME yoursite.b-cdn.net. In the BunnyCDN dashboard, go to your Pull Zone, Hostnames, and addcdn.yoursite.nl. Wait for DNS propagation, then enable the free Bunny SSL certificate for the custom hostname. Branding aside, the practical reason for a custom hostname is that you control DNS and can move the asset traffic to a different CDN later without changing every URL on the site. - Install URL rewriting in WordPress. Either use the official "bunny.net" WordPress plugin (search the directory for "bunny.net") or use the generic CDN Enabler plugin described in the rewriting section below. Either way, the configuration is the same: enter the CDN hostname (
cdn.yoursite.nl) and choose which file types to rewrite.
Verify it worked. Load a page on your site, view source, and confirm that <img> src attributes for images in wp-content/uploads now point at cdn.yoursite.nl instead of yoursite.nl. Request the same asset twice with curl -I https://cdn.yoursite.nl/wp-content/uploads/..., and on the second request the response headers should include a CDN-Cache: HIT or similar header from BunnyCDN's edge nodes confirming the file is being served from cache rather than fetched from your origin. You will know it worked when the BunnyCDN dashboard's bandwidth graph starts climbing within a few minutes of normal traffic.
There is no verifiable evidence of any breaking change to the BunnyCDN Pull Zone API in 2024 or 2025. The API structure and dashboard flow described above are consistent with current documentation.
Setting up KeyCDN
KeyCDN is the other widely-used pull-zone CDN in the WordPress space, and the company also maintains the CDN Enabler plugin that most pull-zone setups use.
- Create a KeyCDN account and add a new Pull Zone from the dashboard. Required fields are Zone Name, Zone Status, Zone Type (set to "Pull"), and Origin URL (set to
https://yoursite.nl). - Take note of the assigned Zone URL, which will look like
yoursite-abc123.kxcdn.com. - Add a Zone Alias (custom hostname) if you want one. KeyCDN's official CDN Enabler integration guide walks through this step. Add a CNAME in your DNS (
cdn.yoursite.nl CNAME yoursite-abc123.kxcdn.com), then add the same hostname as a Zone Alias inside the KeyCDN dashboard, then issue a free Let's Encrypt certificate for it from KeyCDN's SSL section. - Install CDN Enabler in WordPress and configure it with the CDN hostname (see the next section).
Verify it worked. Same approach as BunnyCDN: view source, confirm asset URLs point at the CDN hostname, request an asset twice with curl, and watch the KeyCDN dashboard for bandwidth.
CDN Enabler, WP Rocket, and W3 Total Cache: the URL rewriting layer
The CDN itself does not rewrite URLs in WordPress. WordPress writes asset URLs that point at your origin, and a WordPress plugin rewrites them to point at the CDN hostname before the page is sent to the visitor. There are three common plugins that do this job, and any of them works with any pull-zone CDN.
CDN Enabler (by KeyCDN) is the lightest option. It is free, open source on GitHub, and actively maintained (last forum activity March 2026). Configuration lives at Settings, CDN Enabler. There are exactly two fields that matter: CDN Hostname (set to cdn.yoursite.nl or whatever you chose) and Included File Extensions (the defaults bmp,bz2,css,gif,gz,... are correct for almost every site). The plugin intercepts the full HTML response via an output buffer, runs a string replacement, and rewrites every asset URL it finds. CDN-agnostic by design, so it works with BunnyCDN, KeyCDN, CloudFront, R2, or anything else that gives you a hostname.
WP Rocket's CDN tab does the same job inside WP Rocket. CDN, Enable Content Delivery Network, then enter the CNAME under "CDN CNAME(s)" and select which content types to rewrite (images, JS, CSS). WP Rocket's own documentation covers the same flow. RocketCDN, the optional WP Rocket addon, is BunnyCDN white-labeled and subscribable directly from the same tab.
W3 Total Cache's CDN tab also does it. Performance, CDN, enable, set CDN type to "Generic Mirror" (or pick the named provider if it is in the list), and enter the CDN hostname. W3 Total Cache is the most configurable of the three and the most complex; it is overkill if URL rewriting is the only feature you need from it.
Pick one rewriting layer, never two. If WP Rocket is rewriting URLs, do not also enable CDN Enabler. They will both try to manipulate the same output buffer and the result is unpredictable, including double-rewritten URLs that point at cdn.yoursite.nl/cdn.yoursite.nl/wp-content/uploads/... and stop loading entirely.
The Cloudflare exception applies here too. If you are running Cloudflare as your reverse proxy, do not enable WP Rocket's CDN tab and do not install CDN Enabler. Cloudflare is already caching assets at the edge under your own domain. WP Rocket is Cloudflare-compatible specifically because of this; the CDN tab is for a separate CDN, not for Cloudflare proxy mode.
For the truly programmatic case (no plugin, code-only), WordPress exposes two filters that CDN plugins hook into:
// wp-content/mu-plugins/cdn-rewrite.php
// Rewrite media library URLs to the CDN hostname.
add_filter( 'wp_get_attachment_url', function ( $url ) {
return str_replace( home_url(), 'https://cdn.yoursite.nl', $url );
} );
// Rewrite responsive image srcset entries.
add_filter( 'wp_calculate_image_srcset', function ( $sources ) {
foreach ( $sources as &$source ) {
$source['url'] = str_replace(
home_url(),
'https://cdn.yoursite.nl',
$source['url']
);
}
return $sources;
} );
This rewrites attachment URLs and responsive srcset entries, but it does not touch enqueued CSS and JS, theme background images in CSS, or anything outside the media library. The plugin approach is more complete for most sites. The hooks themselves are documented at wp_get_attachment_url and wp_calculate_image_srcset.
A note on the UPLOADS constant in wp-config.php: if you have read older articles that present define( 'UPLOADS', ... ) as a CDN setup step, it is not. The constant is a relative path override (always relative to ABSPATH, no leading slash) for where WordPress writes uploaded files locally. It is documented in the WordPress wp-config.php reference as a multisite override and predates almost every CDN plugin. It controls local upload paths, not how those files are served to visitors. Skip it for CDN purposes.
CloudFront: the AWS path
CloudFront is the right choice when you are already on AWS, when you want flat-rate pricing instead of pay-as-you-go, or when you need WAF and DDoS bundled with the CDN. The setup pattern is the same as BunnyCDN and KeyCDN at the WordPress level (create a distribution, point it at your origin, configure CDN Enabler with the distribution hostname), but the AWS side is more configuration than the bunny.net dashboard.
A short note on pricing, since this changes more often than the rest of CloudFront. The traditional pay-as-you-go model is still available. In November 2025, AWS added flat-rate pricing plans: Free ($0), Pro ($15/month), Business ($200/month), and Premium ($1,000/month). The flat-rate plans bundle CDN with WAF, DDoS protection, Route 53, CloudWatch, an ACM TLS certificate, edge compute, and S3 storage credits. Whether the bundle is cheaper than a pure pull-zone CDN like BunnyCDN depends entirely on traffic and what else you would buy from AWS anyway.
Cloudflare R2 for WordPress media offload
Cloudflare R2 is Cloudflare's S3-compatible object storage product, and it has become a popular target for WordPress media offload because R2 egress bandwidth is free. Storage is $0.015 per GB per month, period. There is no per-GB transfer fee, so a media library that gets a lot of image traffic costs roughly the storage of the files themselves and nothing for the bandwidth.
The setup model is different from a pull-zone CDN. Instead of caching your origin's responses at an edge, you migrate (offload) your media library out of wp-content/uploads into an R2 bucket. WordPress then writes new uploads to R2 directly and rewrites attachment URLs to point at the R2 bucket's custom domain. Your origin server stops storing media at all.
Several WordPress plugins implement this pattern:
- Codirun R2 Media & Static CDN
- Yctvn Media Offload for Cloudflare R2
- Media Cloud (formerly ilab-media-tools)
- Next3 Offload
The general flow is: install one of these plugins, create an R2 bucket and an R2 API token in the Cloudflare dashboard, configure the plugin with the bucket name and credentials, and let it migrate the existing media library in the background. New uploads from that point on land in R2 directly. You then attach a custom domain to the bucket (R2 supports custom domains natively) so attachment URLs point at media.yoursite.nl rather than the raw R2 URL. R2 buckets sit behind Cloudflare's edge network automatically when accessed via a custom domain, so you get edge caching for free on top of the offload.
For high-traffic media-heavy sites the math is compelling. A community guide on offloading WordPress media to R2 reports TTFB on media requests dropping from 180–400 ms at the origin to 15–45 ms from the edge after the migration. The trade-off is that your media is now in a different system from your WordPress install, so backups, restores, and disaster recovery have to account for two locations instead of one.
Excluding dynamic pages from the CDN
A pull-zone CDN with URL rewriting only rewrites static asset URLs, not WordPress HTML, so the dynamic-pages problem mostly does not exist in the pull-zone model. WordPress generates HTML at the origin every time, including pages like /checkout/, /cart/, /my-account/, and /wp-admin/. The CDN never sees those HTML responses because the URL rewriting only affects <img>, <link href="...">, and <script src="..."> attributes for files in wp-content.
There is still one thing to think about. Some asset URL rewriters can be too aggressive and try to rewrite admin assets too. If you log in to wp-admin and see your dashboard load a CSS file from cdn.yoursite.nl/wp-admin/css/..., that admin CSS is being served from the CDN cache and you risk serving stale admin styling after a WordPress core update. CDN Enabler's default include list avoids this because the rewriter respects the file extension list and admin paths still resolve, but if you have customized the rules it is worth verifying.
If you are running a full proxy CDN (Cloudflare with HTML caching turned on, or APO), the dynamic pages story is completely different and you must explicitly bypass the cache for /wp-admin/, /wp-login.php, /wp-json/, the wordpress_logged_in_* cookies, and any WooCommerce session cookies. That is the Cloudflare configuration article's territory, not this one.
Testing CDN delivery from the command line
Once URL rewriting is configured and the CDN is in front, test it with curl rather than relying on the browser, because the browser cache lies to you about whether the CDN is actually serving anything.
# 1) Confirm the CDN hostname resolves and serves the file.
curl -I https://cdn.yoursite.nl/wp-content/uploads/2026/04/example.jpg
# Expected output (abbreviated):
# HTTP/2 200
# content-type: image/jpeg
# cdn-cache: MISS # first request after cache eviction
# server: BunnyCDN-...
# cache-control: public, max-age=...
# 2) Request the same file again to confirm it is now cached at the edge.
curl -I https://cdn.yoursite.nl/wp-content/uploads/2026/04/example.jpg
# Expected output (abbreviated):
# HTTP/2 200
# cdn-cache: HIT # served from edge cache, no origin hit
The header that confirms the cache hit is provider-specific. BunnyCDN sends CDN-Cache: HIT, KeyCDN sends X-Cache: HIT, CloudFront sends X-Cache: Hit from cloudfront, and Cloudflare (when proxying assets directly) sends cf-cache-status: HIT. You will know the CDN is working when the second request returns the HIT header and the response time drops by an order of magnitude compared to the first request.
A second test, this time on the WordPress page itself, confirms the URL rewriting is in place:
curl -s https://yoursite.nl/ | grep -oE 'src="https://[^"]+"' | head -10
If the rewriter is doing its job, the asset URLs in the homepage HTML should mostly point at cdn.yoursite.nl, not at yoursite.nl. If they still point at yoursite.nl, the rewriting plugin is not active or its include list does not match the file types you are looking for.
Common things that go wrong, and where to look first
Three failure modes account for almost every "I set up a CDN and now my site is broken" question I see.
Mixed content warnings on HTTPS pages. The CDN hostname is configured as HTTP (http://cdn.yoursite.nl) but your WordPress site is HTTPS. Browsers block mixed content. Fix: change the CDN hostname in your rewriter plugin to https://cdn.yoursite.nl and make sure you have issued an SSL certificate for the CDN hostname through BunnyCDN, KeyCDN, or whichever provider you are using.
Stale assets after a WordPress or theme update. You changed style.css in your theme, the CDN is still serving the old version. Either purge the asset from the CDN dashboard, or rely on the cache-busting query string that WordPress appends to enqueued assets (?ver=6.8) to bust the cache automatically when the file's version changes. CDN Enabler respects query strings by default. Theme developers who hardcode asset URLs without wp_enqueue_style skip this protection and have to purge manually.
Cloudflare and a pull-zone CDN both rewriting URLs. This is the architectural mistake from earlier in the article. If you are on Cloudflare and you also turn on CDN Enabler with a cdn.yoursite.nl hostname, assets get served from a hostname Cloudflare does not own, the cache is fragmented, and the page slows down rather than speeding up. Pick one. The fix is to disable the pull-zone setup and let Cloudflare's automatic static caching do the job, or to disable Cloudflare's orange cloud on the asset hostname (which is not how Cloudflare wants to be used).
How to verify the whole setup is doing its job
After everything above, run this final check:
- Open the site in a private browser window with DevTools open and the Network panel filtered to images.
- Confirm every image request goes to your CDN hostname, not to the origin.
- Confirm the response headers on those images show a cache HIT after the first request.
- Confirm the page's TTFB in DevTools (the "Waiting" phase on the document request) is roughly the same as before the CDN was installed, because your origin still renders the HTML.
- Confirm the total page transfer size and the time-to-fully-loaded numbers have dropped, because images and assets are now loading from the edge.
If any of those four are wrong, the CDN is not in front of the assets you think it is. Walk back through the URL rewriting plugin configuration, the include list of file extensions, and the CDN hostname's DNS and SSL setup before assuming the CDN itself is broken.