biotech SEO & Performance

Static File Caching with Nginx: Brotli, HTTP/3, and CDN Guide

AK
Ali Kasımoğlu
22 Jan 2021 schedule 5 min read
Nginx Static File Caching: Brotli, HTTP/3, CDN Guide - AnomixLabs
analytics

Insight Density

groups Target Audience: Intermediate
65 Score

Calculated by technical complexity and content density.

Last updated: April 2026 · AnomixLabs Tech Team

Static file caching is the lowest-cost optimization that reduces server costs, increases page speed, and improves user experience — if configured correctly.

Why Cache Static Files?

In a typical web page, 60-80% of network traffic consists of static files (CSS, JS, images, fonts). Caching these files:

  • Reduces page load time by 70-90% on repeat visits
  • Decreases server bandwidth usage
  • Increases Lighthouse Performance score
  • Improves Core Web Vitals (especially LCP)

Cache-Control Headers

Basic Nginx static file caching configuration:

# /etc/nginx/sites-available/anomix.conf

server {
    listen 443 ssl http2;
    server_name anomixlabs.com;

    # Static files — long cache
    location /static/ {
        alias /var/www/anomix/staticfiles/;
        expires 1y;
        add_header Cache-Control 'public, max-age=31536000, immutable';
        add_header Vary Accept-Encoding;
        access_log off;
        gzip_static on;
    }

    # Media files — short cache
    location /media/ {
        alias /var/www/anomix/media/;
        expires 30d;
        add_header Cache-Control 'public, max-age=2592000';
        access_log off;
    }
}

The immutable Directive: Powerful Optimization

The Cache-Control: immutable directive tells the browser, 'this file will never change, don't even ask during refresh.' This eliminates unnecessary conditional requests (304 Not Modified) during soft reloads (F5).

Condition: Immutable should only be used with file names containing a content hash. With Django:

# settings.py
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# This setting adds hashes to filenames during collectstatic:
# main.css → main.3a7f9b2c.css
# This way, when the content changes, the URL changes, and the cache is invalidated seamlessly

Brotli vs Gzip: Which is Better?

Brotli (Google, 2015) offers a better compression ratio than gzip — but at a higher CPU cost:

  • Text files (HTML/CSS/JS): Brotli compresses 15-25% better than gzip
  • For static files: Pre-compressed .br file — no CPU cost
  • For dynamic content: gzip is more practical (faster encoding)
  • Browser support: As of 2026, Brotli has 97%+ browser support
# Nginx Brotli module (nginx-module-brotli)
brotli on;
brotli_comp_level 6;           # Between 1-11, 6 is a good balance
brotli_static on;              # Pre-compressed .br files
brotli_types text/plain text/css application/javascript
             application/json image/svg+xml;

# Gzip fallback
gzip on;
gzip_static on;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;

Nginx with HTTP/3 (QUIC)

HTTP/3 overcomes some limitations of HTTP/2 by using the UDP-based QUIC protocol — especially noticeable performance improvements on networks with packet loss:

# HTTP/3 support with Nginx 1.25+
server {
    listen 443 ssl;
    listen 443 quic reuseport;  # QUIC for HTTP/3
    http2 on;

    ssl_certificate /etc/letsencrypt/live/site.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/site.com/privkey.pem;

    # Inform the browser about HTTP/3 support
    add_header Alt-Svc 'h3=":443"; ma=86400';
}

Note: Nginx mainline (1.25+) or Nginx Plus is required for HTTP/3. It's available in the Ubuntu 22.04+ package repository. HTTP/3 is automatically enabled behind a Cloudflare CDN.

Nginx map Directive for WEBP and AVIF

Serve modern image formats based on browser support:

# Select modern format based on browser Accept header
map $http_accept $img_suffix {
    default         '';
    '~*avif'        '.avif';
    '~*webp'        '.webp';
}

server {
    location ~* \.(jpe?g|png)$ {
        add_header Vary Accept;
        try_files $uri$img_suffix $uri =404;
    }
}

With this configuration, browsers supporting AVIF will receive .avif, those supporting WebP will receive .webp, and others will get the original JPEG/PNG. To add AVIF/WebP generation to the Django collectstatic pipeline, django-imagekit or Pillow can be used.

Django collectstatic and CI/CD Integration

collectstatic should be automated in the production deployment process:

# .github/workflows/deploy.yml
- name: Collect static files
  run: |
    python manage.py collectstatic --noinput
    # ManifestStaticFilesStorage generates hashes in this step

- name: Deploy to server
  run: |
    rsync -avz staticfiles/ user@server:/var/www/app/staticfiles/
    # No Nginx restart needed — files changed, so URLs did too

WhiteNoise alternative: For small to medium-sized projects, django-whitenoise serves static files from the Python process without Nginx. Compression and caching are automatic. It eliminates Nginx configuration complexity.

# settings.py — WhiteNoise
MIDDLEWARE = [
    'whitenoise.middleware.WhiteNoiseMiddleware',  # Immediately after SecurityMiddleware
    ...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# This setting handles both hashed filenames and Brotli/gzip compression

Nginx Cache Integration with CDN

CDNs like Cloudflare, AWS CloudFront, or BunnyCDN sit in front of Nginx:

  • Origin Pull (Lazy Caching): The CDN fetches from the origin on the first request, then serves from the CDN. The most common mode.
  • Push (Proactive Caching): Files are pushed to the CDN during deployment. Full control with CI/CD.
  • Cache invalidation: Clear the CDN cache after a new deployment — Zone Purge in Cloudflare, Create Invalidation in CloudFront.
# Cloudflare cache purge (add to deploy script)
curl -X POST 'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache' \
  -H 'Authorization: Bearer {API_TOKEN}' \
  -H 'Content-Type: application/json' \
  --data '{"purge_everything": true}'

Lighthouse Cache Audit

The Lighthouse warning 'Serve static assets with an efficient cache policy' lists static files with a cache duration shorter than 1 year. Fix: For all files under /static/, use max-age=31536000 (1 year) + immutable.

If you are using ManifestStaticFilesStorage for cache busting, you can completely bypass this Lighthouse warning.

Summary

The basic recipe for Nginx static caching: hashed filenames with ManifestStaticFilesStorage + Cache-Control max-age=31536000 + immutable. An additional 15-25% size saving with Brotli static compression. HTTP/3 can be configured in modern Nginx. Adding a CDN provides global distribution and reduced origin load as a bonus.

Frequently Questions

What happens if CDN and Nginx cache conflict? expand_more
Conflicts typically appear as stale content — a file updated after deployment remains cached in the CDN in its old version. Prevention: use ManifestStaticFilesStorage to generate content-hashed URLs (style.abc123.css). The CDN treats every new URL as a fresh asset — no manual purge needed. This is the most robust cache invalidation strategy for static files.
Should I cache media files too? expand_more
Yes, but with shorter TTLs. User-uploaded files can change (profile photo updates). Recommendation: /media/ with max-age=2592000 (30 days) without immutable — unlike static files, don't make media immutable. If the media file URL includes a hash (e.g., from ImageField upload_to with a UUID), longer caching is safe because the URL changes when the file changes.
How do I integrate collectstatic into CI/CD? expand_more
Add it to the 'build' step of your deploy pipeline, before the 'deploy' step. Order matters: 1) install pip requirements, 2) run python manage.py collectstatic --noinput, 3) ManifestStaticFilesStorage generates hashed filenames in STATIC_ROOT, 4) rsync or S3 sync pushes them to the CDN origin. Running collectstatic in the 'deploy' step risks serving a mix of old and new asset URLs during the deployment window.
What is a cache invalidation strategy? expand_more
Three approaches: 1) Content hash URL (best — Django ManifestStaticFilesStorage): URL changes when file changes, old cache automatically invalid. 2) Versioned URL (?v=2.1.0): requires manual updates, easy to forget. 3) Time-based expiry (max-age): simplest but can serve stale content until TTL expires. For static files, content hash URLs are the gold standard. For API responses, Cache-Control: no-cache with ETags is appropriate.
Is WhiteNoise sufficient or is Nginx static serving mandatory? expand_more
WhiteNoise is sufficient and simpler for small-to-medium scale. With CompressedManifestStaticFilesStorage, it handles Brotli compression and hash URLs automatically. Nginx static serving is advantageous when: very high traffic (Python process shouldn't be serving files), you need fine-grained cache control headers per file type, or you're using Nginx as a reverse proxy anyway and want to offload static serving entirely from the application server.
What do gzip_static and brotli_static do in Nginx? expand_more
Instead of compressing files on each request, gzip_static and brotli_static serve pre-compressed .gz and .br files. This saves CPU — compression happens once during collectstatic, not on every request. Setup: run collectstatic to generate hashed files, then pre-compress them with gzip -k and brotli --keep. Pair with the immutable Cache-Control directive for maximum efficiency.
How much performance improvement does enabling HTTP/3 provide? expand_more
In high packet-loss environments (mobile, weak WiFi), HTTP/3 makes a noticeable difference — QUIC eliminates HTTP/2's head-of-line blocking at the transport layer. On good network conditions, the difference is minimal. Cloudflare's 2023 data shows 13% average improvement in TTFB for mobile users. For most Django deployments, the effort-to-gain ratio favors focusing on LCP, CLS, and asset optimization before investing in HTTP/3 infrastructure.
Tags: #Nginx #Cache #Statik Dosyalar #Brotli #HTTP/3 #CDN #Django #WhiteNoise #collectstatic #Performans
share

Share This Article

Support us by sharing this with your network.

AK

Ali Kasımoğlu

Full-stack Developer & Founder of AnomixLabs

A software developer specializing in the Python and Django ecosystem. Focuses on modern web architectures, AI integrations, and minimalist user experiences. Under the AnomixLabs umbrella, he aims to transform complex problems into lean and effective digital solutions.

psychology
psychology

Ask a Question About the Article

AnomixAI · Answers based on the article content

5 questions left
Only about article content 0/500
forward_to_inbox

The Future Decoded.

Join 5,000+ engineers and founders receiving the monthly briefing on enterprise AI, software architecture and digital transformation. No spam.