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
- Server generates ETag (hash of response content):
ETag: "a4f3d8e2" - Browser caches response with ETag
- Next request includes ETag:
If-None-Match: "a4f3d8e2" - Server compares current content hash:
- Match →
304 Not Modified(0 bytes transferred) - Different →
200 OKwith new content + new ETag
- Match →
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:
- API Response Time Optimization
- Database Query Optimization
- Performance Monitoring Tools
- Load Testing ChatGPT Apps
- Memory Optimization Techniques
External Resources:
- HTTP Caching Guide - MDN Web Docs
- Service Worker API - MDN
- Cache-Control Header Specification - RFC 9111
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.