HTTP Caching Strategies: Cache-Control, ETag & Service Workers for 90% Faster Repeat Visits

HTTP caching is the most impactful performance optimization you'll ever implement. Properly configured caching reduces repeat visit load times by 90%+ while cutting server costs by 60-80%. For ChatGPT apps serving millions of widget impressions daily, caching isn't optional—it's the foundation of scalable performance.

Yet most developers misuse HTTP caching. They set aggressive max-age values that serve stale content, forget ETags entirely, or implement Service Workers that cache API responses that should never be cached. The result: users see outdated data, cache invalidation becomes a nightmare, and performance degrades instead of improving.

This guide reveals production-ready caching strategies used by high-traffic ChatGPT apps processing 100,000+ daily users. You'll learn when to use Cache-Control: immutable, how ETag validation prevents stale content, Service Worker patterns that balance freshness and speed, and cache invalidation techniques that work at scale.

What you'll master:

  • Cache-Control directives that reduce repeat visit latency from 2000ms to 200ms
  • ETag validation that guarantees content freshness without sacrificing speed
  • Service Worker caching strategies (cache-first, network-first, stale-while-revalidate)
  • Immutable asset patterns that achieve 100% cache hit rates for static files
  • Cache purge strategies that instantly invalidate outdated content across CDN edges
  • Performance monitoring that tracks cache effectiveness and identifies bottlenecks

For comprehensive context on ChatGPT app performance, see our ChatGPT App Performance Optimization: Complete Guide. This article dives deep into HTTP caching specifics that complement broader optimization strategies.


1. Cache-Control Headers: The Foundation of HTTP Caching

The Cache-Control header tells browsers and CDNs how to cache HTTP responses. Incorrect configuration causes either excessive cache misses (poor performance) or stale content (broken UX).

Understanding Cache-Control Directives

max-age=<seconds>: How long the response stays fresh before revalidation is required.

Cache-Control: max-age=3600

This tells browsers/CDNs: "This response is fresh for 1 hour (3600 seconds). Don't make another request until then."

immutable: Signals the resource will NEVER change. Perfect for fingerprinted assets.

Cache-Control: max-age=31536000, immutable

Browsers skip revalidation checks even when users force-refresh. Used with versioned filenames like app-v3.2.1.js or hero-a4f3d8e2.webp.

no-cache: Forces revalidation on every request (but still allows caching).

Cache-Control: no-cache

Browsers must check with the server before using cached content. Pair with ETags for conditional requests.

no-store: Disables caching entirely. Use for sensitive data.

Cache-Control: no-store, private

Nothing is cached—not by browsers, CDNs, or proxies. Use for authentication responses, user profile data, or payment information.

public vs private: Controls who can cache the response.

Cache-Control: public, max-age=3600    # CDNs + browsers can cache
Cache-Control: private, max-age=3600   # Only browsers can cache (skip CDN)

Production-Ready Cache-Control Middleware

Here's an Express.js middleware that applies intelligent caching based on content type and route patterns:

// middleware/cache-control.js
/**
 * Intelligent Cache-Control middleware for ChatGPT apps
 * Applies caching strategies based on content type and route patterns
 */

const crypto = require('crypto');

class CacheControlMiddleware {
  constructor(options = {}) {
    this.options = {
      // Default TTLs (in seconds)
      staticAssets: 31536000,      // 1 year for fingerprinted assets
      images: 2592000,              // 30 days for images
      apiResponses: 300,            // 5 minutes for API data
      htmlPages: 0,                 // No cache for HTML (use ETag validation)

      // CDN integration
      cdnEnabled: true,

      // Override defaults
      ...options
    };
  }

  /**
   * Main middleware function
   */
  handle() {
    return (req, res, next) => {
      // Store original res.send to intercept responses
      const originalSend = res.send.bind(res);

      res.send = (body) => {
        // Determine caching strategy based on request
        const cacheHeaders = this.determineCacheStrategy(req, res);

        // Apply headers
        Object.entries(cacheHeaders).forEach(([header, value]) => {
          res.setHeader(header, value);
        });

        // Generate ETag if applicable
        if (this.shouldGenerateETag(req, res)) {
          const etag = this.generateETag(body);
          res.setHeader('ETag', etag);

          // Check If-None-Match for conditional requests
          if (req.headers['if-none-match'] === etag) {
            return res.status(304).end();
          }
        }

        // Call original send
        return originalSend(body);
      };

      next();
    };
  }

  /**
   * Determine caching strategy based on request/response characteristics
   */
  determineCacheStrategy(req, res) {
    const path = req.path;
    const contentType = res.getHeader('Content-Type') || '';

    // Fingerprinted static assets (immutable)
    if (this.isFingerprintedAsset(path)) {
      return {
        'Cache-Control': `public, max-age=${this.options.staticAssets}, immutable`,
        'Vary': 'Accept-Encoding'
      };
    }

    // Images (long cache with revalidation)
    if (this.isImage(contentType)) {
      return {
        'Cache-Control': `public, max-age=${this.options.images}`,
        'Vary': 'Accept'
      };
    }

    // API responses (short cache, must-revalidate)
    if (path.startsWith('/api/') || path.startsWith('/mcp/')) {
      // Check if response is cacheable (GET only, no user-specific data)
      if (req.method !== 'GET' || this.isUserSpecific(req)) {
        return {
          'Cache-Control': 'no-store, private'
        };
      }

      return {
        'Cache-Control': `public, max-age=${this.options.apiResponses}, must-revalidate`,
        'Vary': 'Accept, Accept-Encoding'
      };
    }

    // HTML pages (no-cache with ETag validation)
    if (contentType.includes('text/html')) {
      return {
        'Cache-Control': 'no-cache, public',
        'Vary': 'Accept-Encoding'
      };
    }

    // Default: no caching
    return {
      'Cache-Control': 'no-store, private'
    };
  }

  /**
   * Check if asset has fingerprinted filename
   * Examples: app-a4f3d8e2.js, styles-v3.2.1.css, hero-12345678.webp
   */
  isFingerprintedAsset(path) {
    const fingerprintPatterns = [
      /\.[a-f0-9]{8,}\.(js|css|webp|png|jpg|svg|woff2?)$/i,  // Hash-based: app-a4f3d8e2.js
      /\-v\d+\.\d+\.\d+\.(js|css)$/i,                         // Version-based: app-v3.2.1.js
    ];

    return fingerprintPatterns.some(pattern => pattern.test(path));
  }

  /**
   * Check if content type is an image
   */
  isImage(contentType) {
    return /^image\//i.test(contentType);
  }

  /**
   * Check if request contains user-specific data
   */
  isUserSpecific(req) {
    // User-specific indicators
    const hasAuthToken = req.headers.authorization || req.cookies?.auth_token;
    const hasUserId = req.query.userId || req.params.userId;

    return !!(hasAuthToken || hasUserId);
  }

  /**
   * Determine if ETag should be generated
   */
  shouldGenerateETag(req, res) {
    const contentType = res.getHeader('Content-Type') || '';
    const cacheControl = res.getHeader('Cache-Control') || '';

    // Don't generate ETags for:
    // - Responses already using immutable caching
    // - Binary content (already fingerprinted)
    // - Responses with no-store directive

    if (cacheControl.includes('immutable') || cacheControl.includes('no-store')) {
      return false;
    }

    // Generate ETags for:
    // - HTML pages
    // - JSON API responses
    // - CSS/JS without fingerprinting

    return (
      contentType.includes('text/html') ||
      contentType.includes('application/json') ||
      contentType.includes('text/css') ||
      contentType.includes('application/javascript')
    );
  }

  /**
   * Generate strong ETag using SHA-256 hash
   */
  generateETag(content) {
    const hash = crypto
      .createHash('sha256')
      .update(content)
      .digest('hex')
      .substring(0, 16);

    return `"${hash}"`;
  }
}

// Export middleware factory
module.exports = (options) => {
  const middleware = new CacheControlMiddleware(options);
  return middleware.handle();
};

// Usage example:
// const cacheControl = require('./middleware/cache-control');
// app.use(cacheControl({ apiResponses: 600 }));

This middleware automatically applies optimal caching strategies based on content type and route patterns. For ChatGPT apps, this means fingerprinted widget assets get immutable caching while API responses use short TTLs with revalidation.


2. ETag Validation: Freshness Without Sacrificing Speed

ETags (Entity Tags) enable conditional requests that prevent stale content while maintaining cache performance. When properly implemented, ETags reduce bandwidth by 95%+ for unchanged resources.

How ETag Validation Works

  1. Server generates ETag (hash of response content): ETag: "a4f3d8e2"
  2. Browser caches response with ETag
  3. Next request includes ETag: If-None-Match: "a4f3d8e2"
  4. Server compares current content hash:
    • Match → 304 Not Modified (0 bytes transferred)
    • Different → 200 OK with new content + new ETag

Strong vs Weak ETags

Strong ETags (byte-for-byte identical):

ETag: "a4f3d8e2"

Weak ETags (semantically equivalent, not byte-identical):

ETag: W/"a4f3d8e2"

Use strong ETags for ChatGPT widget responses where exact content matters. Weak ETags are suitable for human-readable HTML where whitespace differences don't matter.

Production ETag Generator

// utils/etag-generator.ts
import * as crypto from 'crypto';
import * as zlib from 'zlib';

interface ETagOptions {
  algorithm?: 'sha256' | 'md5';
  weak?: boolean;
  includeMetadata?: boolean;
}

interface ETagMetadata {
  lastModified?: Date;
  version?: string;
  userId?: string;
}

/**
 * Production-grade ETag generator for ChatGPT apps
 * Supports strong/weak ETags, metadata inclusion, and compressed content
 */
export class ETagGenerator {
  private algorithm: 'sha256' | 'md5';

  constructor(options: ETagOptions = {}) {
    this.algorithm = options.algorithm || 'sha256';
  }

  /**
   * Generate ETag from content buffer or string
   */
  generate(content: Buffer | string, options: ETagOptions = {}): string {
    const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);

    // Create hash
    const hash = crypto
      .createHash(this.algorithm)
      .update(buffer)
      .digest('hex')
      .substring(0, 16);

    // Weak ETag prefix
    const prefix = options.weak ? 'W/' : '';

    return `${prefix}"${hash}"`;
  }

  /**
   * Generate ETag with metadata (version, user ID, timestamp)
   */
  generateWithMetadata(
    content: Buffer | string,
    metadata: ETagMetadata,
    options: ETagOptions = {}
  ): string {
    const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);

    // Combine content hash with metadata
    const hash = crypto.createHash(this.algorithm);
    hash.update(buffer);

    if (metadata.lastModified) {
      hash.update(metadata.lastModified.toISOString());
    }

    if (metadata.version) {
      hash.update(metadata.version);
    }

    if (metadata.userId) {
      hash.update(metadata.userId);
    }

    const etag = hash.digest('hex').substring(0, 16);
    const prefix = options.weak ? 'W/' : '';

    return `${prefix}"${etag}"`;
  }

  /**
   * Generate ETag for compressed content
   * Uses uncompressed content hash to ensure consistency
   */
  async generateForCompressed(compressedContent: Buffer): Promise<string> {
    return new Promise((resolve, reject) => {
      zlib.gunzip(compressedContent, (err, uncompressed) => {
        if (err) return reject(err);

        const etag = this.generate(uncompressed);
        resolve(etag);
      });
    });
  }

  /**
   * Validate If-None-Match header against current ETag
   * Returns true if content has NOT changed (return 304)
   */
  validate(ifNoneMatch: string | undefined, currentETag: string): boolean {
    if (!ifNoneMatch) return false;

    // Handle multiple ETags in If-None-Match
    const requestETags = ifNoneMatch.split(',').map(tag => tag.trim());

    // Check if any requested ETag matches current
    return requestETags.some(tag => {
      // Weak comparison: ignore W/ prefix
      const normalizedRequest = tag.replace(/^W\//, '');
      const normalizedCurrent = currentETag.replace(/^W\//, '');

      return normalizedRequest === normalizedCurrent;
    });
  }

  /**
   * Generate conditional response headers
   */
  generateConditionalHeaders(etag: string, lastModified?: Date) {
    const headers: Record<string, string> = {
      'ETag': etag
    };

    if (lastModified) {
      headers['Last-Modified'] = lastModified.toUTCString();
    }

    return headers;
  }
}

// Export singleton instance
export const etagGenerator = new ETagGenerator();

// Usage example:
/*
import { etagGenerator } from './utils/etag-generator';

app.get('/api/tools/search', async (req, res) => {
  const data = await searchDatabase(req.query);
  const content = JSON.stringify(data);

  const etag = etagGenerator.generate(content);

  // Check If-None-Match
  if (etagGenerator.validate(req.headers['if-none-match'], etag)) {
    return res.status(304).end();
  }

  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
  res.json(data);
});
*/

For database-driven content, consider including the record's updatedAt timestamp in the ETag metadata. This ensures ETags change immediately when data updates, preventing stale content.


3. Service Worker Caching: Offline-First Performance

Service Workers intercept network requests and apply sophisticated caching strategies. For ChatGPT apps, Service Workers enable instant widget loading on repeat visits by serving cached assets while revalidating in the background.

Cache-First Strategy (Static Assets)

Serve from cache immediately, fall back to network if missing:

// service-worker.js
const CACHE_NAME = 'chatgpt-app-v1.2.3';

const STATIC_ASSETS = [
  '/widget-runtime.js',
  '/styles.css',
  '/icons/chatgpt-icon.svg',
  '/fonts/inter-var.woff2'
];

// Install event: cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
});

// Fetch event: cache-first strategy
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  // Cache-first for static assets
  if (isStaticAsset(url.pathname)) {
    event.respondWith(
      caches.match(event.request).then((cachedResponse) => {
        if (cachedResponse) {
          return cachedResponse;
        }

        // Not in cache, fetch from network
        return fetch(event.request).then((networkResponse) => {
          // Clone response (can only be read once)
          const responseToCache = networkResponse.clone();

          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, responseToCache);
          });

          return networkResponse;
        });
      })
    );
  }
});

function isStaticAsset(pathname) {
  const staticExtensions = ['.js', '.css', '.woff2', '.svg', '.webp', '.png'];
  return staticExtensions.some(ext => pathname.endsWith(ext));
}

Network-First Strategy (API Responses)

Always try network first, fall back to cache on failure (offline support):

// Network-first for API requests
if (url.pathname.startsWith('/api/')) {
  event.respondWith(
    fetch(event.request)
      .then((networkResponse) => {
        // Success: cache response for offline use
        const responseToCache = networkResponse.clone();

        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, responseToCache);
        });

        return networkResponse;
      })
      .catch(() => {
        // Network failed: return cached response
        return caches.match(event.request).then((cachedResponse) => {
          if (cachedResponse) {
            return cachedResponse;
          }

          // No cache available: return offline fallback
          return new Response(
            JSON.stringify({ error: 'Offline', cached: false }),
            {
              status: 503,
              headers: { 'Content-Type': 'application/json' }
            }
          );
        });
      })
  );
}

Stale-While-Revalidate Strategy (Best of Both Worlds)

Serve cached response immediately while fetching fresh content in background:

// Stale-while-revalidate for frequently updated content
if (url.pathname.startsWith('/widgets/')) {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      // Fetch fresh content in background
      const fetchPromise = fetch(event.request).then((networkResponse) => {
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, networkResponse.clone());
        });
        return networkResponse;
      });

      // Return cached response immediately (if available)
      // Otherwise wait for network
      return cachedResponse || fetchPromise;
    })
  );
}

This strategy provides instant perceived performance (cached response) while ensuring eventual consistency (background refresh).


4. Immutable Assets: 100% Cache Hit Rates

Immutable assets never change, enabling aggressive caching with zero revalidation overhead. The pattern: include a content hash in the filename.

Asset Fingerprinting Implementation

// build-tools/asset-fingerprinter.js
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const glob = require('glob');

/**
 * Fingerprint static assets with content hashes
 * Enables immutable caching (Cache-Control: max-age=31536000, immutable)
 */
class AssetFingerprinter {
  constructor(options = {}) {
    this.options = {
      sourceDir: options.sourceDir || './dist',
      outputDir: options.outputDir || './dist',
      hashLength: options.hashLength || 8,
      patterns: options.patterns || ['**/*.{js,css,webp,png,jpg,svg,woff2}'],
      manifestPath: options.manifestPath || './dist/asset-manifest.json',
    };

    this.manifest = {};
  }

  /**
   * Fingerprint all matching assets
   */
  async fingerprint() {
    const files = this.findAssets();

    for (const file of files) {
      await this.fingerprintFile(file);
    }

    // Write manifest for runtime lookups
    this.writeManifest();

    console.log(`✅ Fingerprinted ${files.length} assets`);
    return this.manifest;
  }

  /**
   * Find all assets matching patterns
   */
  findAssets() {
    const files = [];

    this.options.patterns.forEach(pattern => {
      const matches = glob.sync(pattern, {
        cwd: this.options.sourceDir,
        nodir: true
      });

      files.push(...matches);
    });

    return files;
  }

  /**
   * Fingerprint individual file
   */
  async fingerprintFile(relativePath) {
    const sourcePath = path.join(this.options.sourceDir, relativePath);
    const content = fs.readFileSync(sourcePath);

    // Generate hash
    const hash = crypto
      .createHash('sha256')
      .update(content)
      .digest('hex')
      .substring(0, this.options.hashLength);

    // Parse filename
    const parsedPath = path.parse(relativePath);
    const fingerprintedName = `${parsedPath.name}-${hash}${parsedPath.ext}`;
    const fingerprintedPath = path.join(parsedPath.dir, fingerprintedName);

    // Write fingerprinted file
    const outputPath = path.join(this.options.outputDir, fingerprintedPath);
    fs.mkdirSync(path.dirname(outputPath), { recursive: true });
    fs.writeFileSync(outputPath, content);

    // Update manifest
    this.manifest[relativePath] = fingerprintedPath;

    console.log(`  ${relativePath} → ${fingerprintedPath}`);
  }

  /**
   * Write manifest JSON for runtime asset lookups
   */
  writeManifest() {
    fs.writeFileSync(
      this.options.manifestPath,
      JSON.stringify(this.manifest, null, 2)
    );
  }

  /**
   * Helper: Resolve fingerprinted asset path at runtime
   */
  static resolve(assetPath, manifest) {
    return manifest[assetPath] || assetPath;
  }
}

module.exports = AssetFingerprinter;

// CLI usage:
// node build-tools/asset-fingerprinter.js

if (require.main === module) {
  const fingerprinter = new AssetFingerprinter({
    sourceDir: './dist',
    patterns: ['**/*.{js,css,webp,png,jpg,woff2}']
  });

  fingerprinter.fingerprint();
}

Runtime Asset Resolution

// utils/asset-resolver.ts
import manifest from '../dist/asset-manifest.json';

/**
 * Resolve original asset path to fingerprinted version
 */
export function resolveAsset(assetPath: string): string {
  return manifest[assetPath] || assetPath;
}

// Usage in templates:
// <script src="${resolveAsset('widget-runtime.js')}"></script>
// Output: <script src="widget-runtime-a4f3d8e2.js"></script>

With fingerprinted assets, configure your CDN or server to send:

Cache-Control: public, max-age=31536000, immutable

Browsers will NEVER revalidate these files, achieving 100% cache hit rates on repeat visits.


5. Cache Invalidation: Purge Strategies That Work

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

Time-Based Invalidation (Simple)

Set max-age based on content update frequency:

Cache-Control: public, max-age=300    # 5 minutes (frequently updated)
Cache-Control: public, max-age=3600   # 1 hour (hourly updates)
Cache-Control: public, max-age=86400  # 24 hours (daily updates)

Version-Based Invalidation (Recommended)

Change the asset URL when content changes:

// Before: /api/tools/search → cached indefinitely
// After: /api/v2/tools/search → new cache key

For ChatGPT apps, version your MCP server API endpoints. When breaking changes occur, increment the version to bypass all caches.

CDN Cache Purge Implementation

// utils/cache-purger.ts
import axios from 'axios';

interface PurgeOptions {
  provider: 'cloudflare' | 'cloudfront' | 'fastly';
  apiKey: string;
  zoneId?: string;
  distributionId?: string;
}

/**
 * Multi-provider CDN cache purge utility
 */
export class CachePurger {
  private provider: string;
  private apiKey: string;
  private zoneId?: string;
  private distributionId?: string;

  constructor(options: PurgeOptions) {
    this.provider = options.provider;
    this.apiKey = options.apiKey;
    this.zoneId = options.zoneId;
    this.distributionId = options.distributionId;
  }

  /**
   * Purge specific URLs from CDN cache
   */
  async purgeUrls(urls: string[]): Promise<void> {
    switch (this.provider) {
      case 'cloudflare':
        return this.purgeCloudflare(urls);
      case 'cloudfront':
        return this.purgeCloudFront(urls);
      case 'fastly':
        return this.purgeFastly(urls);
      default:
        throw new Error(`Unknown CDN provider: ${this.provider}`);
    }
  }

  /**
   * Purge entire cache (use sparingly)
   */
  async purgeAll(): Promise<void> {
    console.warn('⚠️  Purging entire cache - use sparingly!');

    switch (this.provider) {
      case 'cloudflare':
        return this.purgeCloudflareAll();
      default:
        throw new Error('purgeAll not implemented for this provider');
    }
  }

  /**
   * Cloudflare cache purge
   */
  private async purgeCloudflare(urls: string[]): Promise<void> {
    const response = await axios.post(
      `https://api.cloudflare.com/client/v4/zones/${this.zoneId}/purge_cache`,
      { files: urls },
      {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json'
        }
      }
    );

    if (!response.data.success) {
      throw new Error(`Cloudflare purge failed: ${JSON.stringify(response.data.errors)}`);
    }

    console.log(`✅ Purged ${urls.length} URLs from Cloudflare`);
  }

  /**
   * Cloudflare purge all
   */
  private async purgeCloudflareAll(): Promise<void> {
    await axios.post(
      `https://api.cloudflare.com/client/v4/zones/${this.zoneId}/purge_cache`,
      { purge_everything: true },
      {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json'
        }
      }
    );

    console.log('✅ Purged all Cloudflare cache');
  }

  /**
   * CloudFront cache invalidation (placeholder)
   */
  private async purgeCloudFront(urls: string[]): Promise<void> {
    // AWS SDK implementation required
    throw new Error('CloudFront purge requires AWS SDK - implement as needed');
  }

  /**
   * Fastly cache purge (placeholder)
   */
  private async purgeFastly(urls: string[]): Promise<void> {
    // Fastly API implementation required
    throw new Error('Fastly purge not implemented - add API integration');
  }
}

// Usage:
// const purger = new CachePurger({
//   provider: 'cloudflare',
//   apiKey: process.env.CLOUDFLARE_API_KEY,
//   zoneId: process.env.CLOUDFLARE_ZONE_ID
// });
//
// await purger.purgeUrls([
//   'https://makeaihq.com/widget-runtime.js',
//   'https://makeaihq.com/api/tools/search'
// ]);

For deployment workflows, purge relevant URLs after each production deploy:

# Post-deployment cache purge
node scripts/purge-cache.js \
  --urls "https://makeaihq.com/widget-runtime.js" \
         "https://makeaihq.com/styles.css"

6. Performance Monitoring: Measure Cache Effectiveness

Track cache hit rates and identify bottlenecks with performance monitoring:

// monitoring/cache-performance-monitor.ts
interface CacheMetrics {
  hits: number;
  misses: number;
  hitRate: number;
  avgHitLatency: number;
  avgMissLatency: number;
  totalRequests: number;
}

/**
 * Monitor cache performance and identify optimization opportunities
 */
export class CachePerformanceMonitor {
  private metrics: Map<string, CacheMetrics> = new Map();

  /**
   * Record cache hit
   */
  recordHit(resource: string, latency: number): void {
    this.updateMetrics(resource, true, latency);
  }

  /**
   * Record cache miss
   */
  recordMiss(resource: string, latency: number): void {
    this.updateMetrics(resource, false, latency);
  }

  /**
   * Update metrics for resource
   */
  private updateMetrics(resource: string, isHit: boolean, latency: number): void {
    let metrics = this.metrics.get(resource);

    if (!metrics) {
      metrics = {
        hits: 0,
        misses: 0,
        hitRate: 0,
        avgHitLatency: 0,
        avgMissLatency: 0,
        totalRequests: 0
      };
      this.metrics.set(resource, metrics);
    }

    metrics.totalRequests++;

    if (isHit) {
      metrics.hits++;
      metrics.avgHitLatency =
        (metrics.avgHitLatency * (metrics.hits - 1) + latency) / metrics.hits;
    } else {
      metrics.misses++;
      metrics.avgMissLatency =
        (metrics.avgMissLatency * (metrics.misses - 1) + latency) / metrics.misses;
    }

    metrics.hitRate = metrics.hits / metrics.totalRequests;
  }

  /**
   * Generate performance report
   */
  generateReport(): string {
    let report = '\n📊 Cache Performance Report\n';
    report += '='.repeat(60) + '\n\n';

    const sortedMetrics = Array.from(this.metrics.entries())
      .sort((a, b) => b[1].totalRequests - a[1].totalRequests);

    sortedMetrics.forEach(([resource, metrics]) => {
      const hitRatePercent = (metrics.hitRate * 100).toFixed(1);
      const speedup = (metrics.avgMissLatency / metrics.avgHitLatency).toFixed(1);

      report += `${resource}\n`;
      report += `  Requests: ${metrics.totalRequests} | Hit Rate: ${hitRatePercent}%\n`;
      report += `  Hits: ${metrics.hits} (${metrics.avgHitLatency.toFixed(0)}ms avg)\n`;
      report += `  Misses: ${metrics.misses} (${metrics.avgMissLatency.toFixed(0)}ms avg)\n`;
      report += `  Speed improvement: ${speedup}x faster on cache hits\n\n`;
    });

    return report;
  }

  /**
   * Identify low-hit-rate resources for optimization
   */
  findOptimizationOpportunities(): string[] {
    const lowHitRateThreshold = 0.5;  // 50%

    return Array.from(this.metrics.entries())
      .filter(([_, metrics]) =>
        metrics.hitRate < lowHitRateThreshold && metrics.totalRequests > 100
      )
      .map(([resource, metrics]) =>
        `${resource} (${(metrics.hitRate * 100).toFixed(1)}% hit rate)`
      );
  }
}

// Global monitor instance
export const cacheMonitor = new CachePerformanceMonitor();

// Usage in middleware:
// const startTime = Date.now();
// const cachedResponse = await cache.get(key);
// const latency = Date.now() - startTime;
//
// if (cachedResponse) {
//   cacheMonitor.recordHit(req.path, latency);
// } else {
//   cacheMonitor.recordMiss(req.path, latency);
// }

Run periodic reports to identify resources with low cache hit rates:

// Generate weekly cache report
setInterval(() => {
  console.log(cacheMonitor.generateReport());

  const opportunities = cacheMonitor.findOptimizationOpportunities();
  if (opportunities.length > 0) {
    console.warn('\n⚠️  Low cache hit rates detected:');
    opportunities.forEach(opp => console.warn(`  - ${opp}`));
  }
}, 7 * 24 * 60 * 60 * 1000);  // Weekly

Resources with hit rates below 50% need investigation—consider adjusting max-age, implementing stale-while-revalidate, or pre-warming the cache.


Conclusion: Caching as Competitive Advantage

HTTP caching transforms ChatGPT app performance from acceptable to exceptional. By implementing Cache-Control headers, ETag validation, Service Worker strategies, immutable assets, and intelligent cache invalidation, you'll achieve:

  • 90%+ faster repeat visits (2000ms → 200ms load times)
  • 60-80% reduced server costs (fewer origin requests)
  • 100% cache hit rates for static assets (zero revalidation overhead)
  • Instant perceived performance with stale-while-revalidate
  • Global sub-200ms latency with CDN edge caching

For comprehensive performance optimization strategies, see our ChatGPT App Performance Optimization: Complete Guide. Combine HTTP caching with CDN configuration, image optimization, and code splitting for maximum impact.

Related Resources:

External Resources:


Ready to achieve 90%+ faster repeat visits? Start building high-performance ChatGPT apps on MakeAIHQ.com — our platform automatically implements production-ready caching strategies, freeing you to focus on building features your users love.

About MakeAIHQ.com: The only no-code platform specifically designed for ChatGPT App Store. From zero to production-ready ChatGPT app in 48 hours — no coding required. Trusted by 5,000+ businesses reaching 800 million ChatGPT users.