Back to Work

Turning WordPress into a Static Site: A Reverse Proxy Caching Architecture

A theoretical architecture for serving 3-second WordPress pages in under 100ms—without headless rewrites, without changing development workflows, and at a fraction of traditional hosting costs.

Note: This is a theoretical architecture I designed but haven't yet implemented in production. The concepts are proven—I've built similar systems for other use cases—but the specific WordPress application remains untested at scale.

The WordPress Performance Problem

WordPress powers over 40% of the web. It's feature-rich, has a massive plugin ecosystem, and content teams love it. But WordPress is also a resource hog. Every page request triggers:

  • PHP bootstrap and WordPress core initialization
  • Database queries—often 50-200+ per page load
  • Plugin hooks and filters (the more plugins, the worse it gets)
  • Theme rendering and template processing
  • Dynamic content assembly

For a typical WordPress site with a dozen plugins and a premium theme, you're looking at 1-3+ seconds per page load. That's before the browser even starts rendering.

The Question That Started This

One weekend I asked myself: "How could I convert a WordPress website into something more like a static HTML/CSS site, while maintaining WordPress functionality, and without altering current development or deployment processes?"

Headless WordPress is the common answer—decouple the frontend, use a static site generator or React framework, pull content via the REST API. But that requires significant development changes, retraining content teams, and often a complete frontend rewrite.

I wanted something more transparent. Something that sits in front of WordPress and makes it appear static without WordPress knowing anything changed.

The Solution: Intelligent Reverse Proxy Caching

The architecture I designed uses OpenResty (Nginx + LuaJIT) as an intelligent caching reverse proxy. It intercepts all requests, determines cacheability, and serves cached responses when possible—eliminating WordPress processing entirely for repeat requests.

Architecture v1: Redis-Backed Caching

The first iteration uses Redis as the caching layer:

┌─────────────────────────────────────────────────────────────────┐
│                         KUBERNETES CLUSTER                       │
│                    4 nodes × (4 vCPU / 8GB RAM)                  │
│                        ~$192/month total                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│    ┌──────────────┐                                              │
│    │ Load Balancer│◄──── User requests (A record points here)   │
│    └──────┬───────┘                                              │
│           │                                                      │
│           ▼                                                      │
│    ┌──────────────┐      ┌──────────────┐                       │
│    │   OpenResty  │◄────►│    Redis     │                       │
│    │  (Lua logic) │      │   (Cache)    │                       │
│    └──────┬───────┘      └──────────────┘                       │
│           │                                                      │
│           │ License     ┌──────────────┐                        │
│           │ lookup      │   MariaDB    │                        │
│           └────────────►│  (Licenses)  │                        │
│                         └──────────────┘                        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
            │
            │ Cache miss: fetch from origin
            ▼
┌─────────────────────────────────────────────────────────────────┐
│                     CUSTOMER'S WORDPRESS                         │
│                   (unchanged, unaware)                           │
│                                                                  │
│    ┌──────────────┐      ┌──────────────┐                       │
│    │  WordPress   │◄────►│    MySQL     │                       │
│    │   Server     │      │   Database   │                       │
│    └──────────────┘      └──────────────┘                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Request Flow

User Request
     │
     ▼
┌─────────────────┐
│ License Check   │──── Invalid? ──► 403 Forbidden
│ (Redis lookup)  │
└────────┬────────┘
         │ Valid
         ▼
┌─────────────────┐
│ Request Type?   │
└────────┬────────┘
         │
    ┌────┴────┬──────────────┐
    │         │              │
    ▼         ▼              ▼
Static    Cacheable      Dynamic
Asset     Content        Request
(CSS/JS)  (blog post)    (/cart, /wp-admin)
    │         │              │
    ▼         ▼              ▼
┌─────────┐ ┌─────────┐  ┌─────────┐
│ Check   │ │ Check   │  │ Bypass  │
│ Redis   │ │ Redis   │  │ Cache   │
└────┬────┘ └────┬────┘  └────┬────┘
     │           │            │
  Hit│Miss    Hit│Miss        │
     │  │        │  │         │
     ▼  ▼        ▼  ▼         ▼
   Serve│      Serve│       Proxy to
   from │      from │       WordPress
   Redis│      Redis│          │
        │           │          │
        ▼           ▼          ▼
     Fetch       Fetch      Return
     from        from       response
     origin      origin     directly
        │           │
        ▼           ▼
     Cache       Cache
     in Redis    in Redis
     (honor TTL) (honor TTL)

Cache Decision Logic

The Lua logic determines cacheability based on several factors:

-- Simplified cache decision logic
local function should_cache(request, response)
    -- Never cache these paths
    local bypass_patterns = {
        "^/wp%-admin",
        "^/wp%-login",
        "^/cart",
        "^/checkout",
        "^/my%-account",
        "^/wp%-json/wc",  -- WooCommerce API
        "^/wp%-ajax%.php"
    }

    for _, pattern in ipairs(bypass_patterns) do
        if ngx.re.match(request.uri, pattern) then
            return false
        end
    end

    -- Don't cache if user is logged in
    if request.cookies["wordpress_logged_in"] then
        return false
    end

    -- Don't cache POST requests
    if request.method ~= "GET" then
        return false
    end

    -- Respect origin's cache-control headers
    local cc = response.headers["Cache-Control"]
    if cc and cc:match("no%-cache") or cc:match("private") then
        return false
    end

    return true
end

Customer Cache Management

Customers would have a dashboard to manage their cache rules:

  • Global bypass patterns: Add custom paths that should never be cached
  • Force cache patterns: Override default behavior for specific paths
  • TTL overrides: Set custom expiration times per path pattern
  • Purge controls: Clear cache for specific URLs or entire site
  • Cache warming: Pre-populate cache for critical pages

The Problem with v1: Redis Limits

This architecture works well for small to medium sites. But it has scaling challenges:

  • Memory constraints: Large sites with thousands of pages and assets can easily exceed Redis memory limits
  • Multi-tenant scaling: Running multiple large sites requires expensive Redis clusters
  • Bandwidth costs: All content flows through your infrastructure—VPS egress adds up fast
  • Geographic latency: Users far from your cluster experience higher TTFB

Architecture v2: CDN-First with Proxy Origin

The second iteration solves these problems by flipping the architecture: instead of Redis being the cache, make the reverse proxy the origin for a CDN.

┌─────────────────────────────────────────────────────────────────┐
│                            CDN EDGE                              │
│                    (Bunny.net, CloudFlare, etc.)                 │
│                                                                  │
│    ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐          │
│    │ Dallas  │  │ London  │  │ Tokyo   │  │ Sydney  │  ...     │
│    │   PoP   │  │   PoP   │  │   PoP   │  │   PoP   │          │
│    └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘          │
│         │            │            │            │                 │
│         └────────────┴─────┬──────┴────────────┘                │
│                            │                                     │
│                      Cache miss                                  │
│                            │                                     │
└────────────────────────────┼────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    REVERSE PROXY CLUSTER                         │
│                   (Origin for CDN)                               │
│                                                                  │
│    ┌──────────────┐      ┌──────────────┐                       │
│    │   OpenResty  │◄────►│   MariaDB    │                       │
│    │ (Cache-Control      │  (Licenses)  │                       │
│    │   policy only)      └──────────────┘                       │
│    └──────┬───────┘                                              │
│           │                                                      │
│           │ NO Redis needed!                                     │
│           │ CDN handles caching                                  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
            │
            │ Fetch from customer origin
            ▼
┌─────────────────────────────────────────────────────────────────┐
│                     CUSTOMER'S WORDPRESS                         │
└─────────────────────────────────────────────────────────────────┘

How v2 Works

  1. DNS Configuration: Customer points their domain to the CDN, not directly to the proxy
  2. CDN Origin: CDN is configured with the reverse proxy cluster as its origin
  3. Request Flow: User → CDN Edge → (cache miss) → Reverse Proxy → WordPress
  4. Proxy Role: The proxy now only handles cache-control policy—it doesn't store anything
  5. CDN Caching: The CDN caches responses based on headers the proxy sets
User in Tokyo
     │
     ▼
┌─────────────────┐
│  CDN Tokyo PoP  │
│  (Edge Cache)   │
└────────┬────────┘
         │
    Cache hit? ────► YES ────► Serve from edge (~20ms)
         │
         NO (cache miss)
         │
         ▼
┌─────────────────┐
│  Reverse Proxy  │ ◄─── Sets Cache-Control headers
│  (US Central)   │      based on request analysis
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   WordPress     │ ◄─── Only hit on true cache misses
│   (Customer)    │
└─────────────────┘
         │
         ▼
Response flows back through proxy → CDN → User
CDN caches the response at edge for future requests

What the Proxy Does in v2

Without Redis, the proxy's job becomes simpler but still critical:

-- v2: Proxy sets cache policy, CDN does the caching
local function process_response(request, response)
    -- Check if this request should bypass CDN cache
    if is_dynamic_request(request) then
        -- Force CDN to not cache
        response.headers["Cache-Control"] = "private, no-store"
        response.headers["CDN-Cache-Control"] = "no-store"
        return
    end

    -- Static assets: long cache
    if is_static_asset(request.uri) then
        response.headers["Cache-Control"] = "public, max-age=31536000"
        return
    end

    -- Blog content: respect origin or set sensible default
    local origin_cc = response.headers["Cache-Control"]
    if not origin_cc or origin_cc == "" then
        -- Default: cache for 1 hour at edge
        response.headers["Cache-Control"] = "public, max-age=3600"
        response.headers["CDN-Cache-Control"] = "max-age=3600"
    end
end

Performance Estimates

Based on industry benchmarks for OpenResty and Nginx:

Metric Estimate Source
OpenResty throughput (single worker) ~14,000 req/sec Conservative estimate
With Redis lookups ~9,000 req/sec Conservative estimate
4-node cluster (16 cores total) 25,000-50,000 req/sec Conservative estimate
Added latency (proxy overhead) <20ms per request Based on similar systems
CDN edge response 10-50ms globally Typical CDN TTFB

User Experience Impact

Scenario Before After (v2)
Blog post (first visitor) 2-3 seconds 2-3 seconds + ~20ms
Blog post (cached) 2-3 seconds 20-50ms
Static assets 200-500ms 10-30ms
Dynamic pages (/cart) 1-2 seconds 1-2 seconds + ~20ms

For content-heavy sites where 90%+ of traffic is to cacheable pages, the improvement is dramatic: pages that took 3 seconds now load in under 100ms.

Cost Analysis

Infrastructure Costs (v2 Architecture)

Component Specs Est. Cost
Kubernetes nodes (4x) 4 vCPU / 8GB each ~$192/mo
Load Balancer Managed LB ~$20/mo
MariaDB (licenses) Minimal instance ~$15/mo
CDN (Bunny.net) Per-GB pricing Variable
Total (before CDN) ~$227/mo

This infrastructure can serve multiple WordPress sites simultaneously. The per-site cost approaches near-zero as you add customers.

Customer Value Proposition

For a WordPress site owner currently paying $100-300/month for managed WordPress hosting:

  • Faster load times: 3 seconds → <100ms for cached content
  • Better SEO: Core Web Vitals improvements
  • Higher conversions: Every 100ms improvement increases conversions
  • Origin relief: WordPress server handles 90% less traffic
  • No code changes: Works with existing site, themes, plugins

Beyond WordPress

While this architecture was designed with WordPress in mind, it's not WordPress-specific. The same approach works for:

  • Any CMS: Drupal, Joomla, custom PHP apps
  • Legacy applications: Old monoliths that can't be easily refactored
  • API responses: Cache GET requests to slow APIs
  • E-commerce product pages: Cache product listings, bypass cart/checkout

Any application where the majority of traffic is to content that doesn't change frequently can benefit from this pattern.

Implementation Considerations

Cache Invalidation

The hardest problem in computer science. Options include:

  • WordPress plugin: Hook into post save/update to trigger CDN purge
  • Webhook integration: Customer's WordPress calls purge API
  • TTL-based expiry: Set reasonable TTLs and accept slight staleness
  • Surrogate keys: Tag cached content for targeted invalidation

Edge Cases

  • Logged-in users: Bypass cache entirely based on cookies
  • A/B testing: Handle via Vary headers or separate cache keys
  • Personalized content: ESI (Edge Side Includes) for partial caching
  • Form submissions: POST requests always bypass cache

Key Takeaways

  • Transparent optimization: The best performance wins are invisible to developers and content teams
  • CDN as cache, proxy as policy: Let the CDN handle storage and distribution; use the proxy for intelligent decision-making
  • Massive ROI: Small infrastructure investment, dramatic performance improvement for customers
  • Multi-tenant economics: Shared infrastructure means per-customer costs approach zero at scale
Key Takeaway

The fastest WordPress page is the one that never hits WordPress at all.

Sources & Benchmarks

Performance estimates in this article are based on published benchmarks: