Back to blog
InfraSeptember 18, 20257 min read

Setting Up Cloudflare HTML Caching Without Breaking Your API

Caching HTML at the edge is great. Accidentally caching your API responses is not. Here is the setup that keeps both working correctly.

Cloudflare by default does not cache HTML responses. It will cache your images, JS, and CSS, but HTML gets a DYNAMIC status and passes through to your origin every time. For a mostly static site this is wasteful, especially when you are paying for origin bandwidth and your LCP scores are being dragged down by the time to first byte.

Setting up HTML caching in Cloudflare is not complicated but there is a specific order you need to do things in, and mixing it up with API routes that should never be cached requires some care.

The Cache Rule setup

Go to your Cloudflare dashboard, select your domain, and open Rules > Cache Rules. Create a new rule with these conditions:

For static pages (your blog posts, landing pages, etc):

Field: URI Path
Operator: starts with
Value: /blog/

Then: Cache eligibility: Eligible for cache
      Edge cache TTL: 1 hour
      Browser TTL: 5 minutes

For your API routes, you need an explicit bypass rule. Make this rule run before the caching rule by giving it a higher priority number:

Field: URI Path
Operator: starts with  
Value: /api/

Then: Cache eligibility: Bypass cache

The order of rules matters in Cloudflare. Rules are evaluated top to bottom and the first match wins. Put your bypass rules above your caching rules.

Origin headers

For Cloudflare to cache your HTML responses, the origin needs to send the right headers. Specifically, Cloudflare respects Cache-Control from the origin. If your nginx is sending no-store or no-cache, Cloudflare will not cache regardless of what your Cache Rules say.

Make sure your HTML responses include:

nginx
add_header Cache-Control "public, max-age=300, s-maxage=3600";

The s-maxage directive is specifically for shared caches like Cloudflare. This says "browsers can cache this for 5 minutes, but CDN/proxy caches can cache this for 1 hour." Your Cache Rule edge TTL will override s-maxage if it is set higher, but having it in the origin header as well is good practice.

Purging the cache after deploys

Once HTML is cached at the edge, you need a way to purge it when you deploy new content. We built a small endpoint in our Flask API for this:

python
import httpx
from flask import jsonify

CF_ZONE_ID = "your_zone_id"
CF_API_TOKEN = "your_api_token"

@app.route("/admin/purge-cache", methods=["POST"])
@require_admin
def purge_cache():
    resp = httpx.post(
        f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/purge_cache",
        headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
        json={"purge_everything": True},
        timeout=10,
    )
    return jsonify(resp.json())

There is a button in our admin panel that calls this after a deploy. Takes about 30 seconds for the purge to propagate globally.

Do not use purge_everything in production at scale because it invalidates your entire cache, which means the next wave of traffic all hits your origin. For a small site it is fine. For something with significant traffic, purge specific URLs or use cache tags instead.

Results

After setting this up, TTFB for cached pages dropped from around 200-400ms to under 50ms for users hitting a warm cache. LCP on mobile improved significantly, which also helps Core Web Vitals scores. The main gotcha to watch for is making sure your admin panel and API routes are explicitly bypassed, otherwise you will have a bad time.

More articles