-
Notifications
You must be signed in to change notification settings - Fork 400
Description
📦 Performance - Static Asset Caching & CDN
Goal
Optimize delivery of static assets (CSS, JS, images, fonts) for the admin UI:
- Add Cache-Control headers for static files with appropriate TTLs
- Implement ETag generation for cache validation
- Add content hashing for cache busting on updates
- Create nginx reverse proxy config template for static asset offloading
- Create Caddy reverse proxy config template as alternative
- Document CDN integration for production deployments
This reduces server load, improves page load times through browser caching, and enables global CDN distribution for faster asset delivery.
Why Now?
Static asset optimization is critical for web application performance:
- Browser Caching: Eliminate repeat downloads, improve repeat visit performance by 80-95%
- Reduced Server Load: Static assets served from CDN or reverse proxy, not Python app
- Faster Page Loads: Cached assets load instantly from browser cache, no network roundtrip
- CDN Benefits: Edge caching brings assets closer to users globally (10-50ms latency)
- Lower Costs: Reduced bandwidth from origin server saves 80-95% bandwidth costs
- Better Mobile Experience: Critical for users on slower/metered connections
📖 User Stories
US-1: Admin UI User - Instant Page Loads on Repeat Visits
As an Admin UI User
I want static assets to be cached in my browser
So that pages load instantly on repeat visits
Acceptance Criteria:
Given I am visiting the Admin UI for the first time
When the page loads
Then all CSS, JS, and image files should be downloaded
And Cache-Control headers should be set with long TTL (1 year)
And ETag headers should be present for validation
Given I am visiting the Admin UI for the second time
When the page loads
Then the browser should use cached static assets
And only HTTP 304 Not Modified responses should be sent
And the page should load 80-95% faster than first visit
And network traffic should be minimal (only HTML and API calls)Technical Requirements:
- Cache-Control: public, max-age=31536000, immutable for static assets
- ETag generation for cache validation
- Vary: Accept-Encoding header for compression compatibility
- Content hashing or versioning for cache busting
US-2: DevOps Engineer - Offload Static Assets to Reverse Proxy
As a DevOps Engineer
I want to serve static assets from nginx/Caddy instead of Python app
So that I can reduce server load and improve performance
Acceptance Criteria:
Given I have deployed nginx reverse proxy
When a request is made for /static/* files
Then nginx should serve the file directly from disk
And the Python application should not be involved
And pre-compressed files (gzip, brotli) should be used if available
Given I have configured Caddy reverse proxy
When a request is made for /static/* files
Then Caddy should serve the file with automatic compression
And Cache-Control headers should be set automatically
And HTTPS should be enabled with automatic certificatesTechnical Requirements:
- nginx configuration template in deployment/nginx/
- Caddy configuration template in deployment/caddy/
- Static file serving with gzip_static and brotli_static
- Documentation for deployment and testing
US-3: Global User - Fast Asset Loading from CDN
As a Global User
I want static assets to be served from a CDN near me
So that the admin UI loads quickly regardless of my location
Acceptance Criteria:
Given I am accessing the gateway from Asia
When I load the Admin UI
Then static assets should be served from Asian CDN edge location
And asset load time should be <100ms (vs 500ms+ from origin)
And bandwidth costs should be reduced by 80-95%
Given static assets are updated
When I visit the Admin UI
Then cache busting should ensure I get the latest version
And no stale assets should be servedTechnical Requirements:
- CDN integration guide (CloudFlare, AWS CloudFront, Fastly)
- Cache-Control headers compatible with CDN caching
- Content hashing or versioning for cache invalidation
- Purging/invalidation procedures documented
🏗 Architecture
Static Asset Delivery Flow
graph TD
A[Browser Request] --> B{Asset Type}
B -->|/static/*| C[Reverse Proxy/CDN]
B -->|/api/*| D[FastAPI App]
B -->|/admin| D
C --> E{Cache Status}
E -->|Cache Hit| F[Serve from Cache]
E -->|Cache Miss| G[Read from Disk]
G --> H[Add Cache-Control Headers]
H --> I[Compress if needed]
I --> J[Return to Browser]
F --> J
D --> K[Dynamic Response]
K --> J
style C fill:#90EE90
style D fill:#FFE4B5
style F fill:#90EE90
Browser Caching Flow
sequenceDiagram
participant Browser
participant Server
Note over Browser,Server: First Visit (Cache Miss)
Browser->>Server: GET /static/css/style.css
Server->>Browser: 200 OK + Cache-Control: max-age=31536000 + ETag: "abc123"
Note over Browser: Store in cache for 1 year
Note over Browser,Server: Second Visit (Cache Hit)
Browser->>Server: GET /static/css/style.css + If-None-Match: "abc123"
Server->>Browser: 304 Not Modified (empty body)
Note over Browser: Use cached version
Note over Browser,Server: After Update (Cache Busted)
Browser->>Server: GET /static/css/style.v2.css
Server->>Browser: 200 OK + Cache-Control + ETag: "def456"
Note over Browser: New version cached
CDN Edge Caching
graph LR
A[User in Asia] --> B[CDN Edge: Tokyo]
C[User in Europe] --> D[CDN Edge: London]
E[User in USA] --> F[CDN Edge: NYC]
B --> G{Cached?}
D --> G
F --> G
G -->|Yes| H[Serve from Edge]
G -->|No| I[Origin Server]
I --> J[Cache at Edge]
J --> H
style H fill:#90EE90
style I fill:#FFE4B5
Implementation Examples
# mcpgateway/utils/cached_static_files.py
from fastapi.staticfiles import StaticFiles
from starlette.responses import Response
import hashlib
from pathlib import Path
class CachedStaticFiles(StaticFiles):
"""
StaticFiles subclass with Cache-Control and ETag headers.
Improves performance through browser caching and cache validation.
"""
def __init__(self, *args, cache_max_age: int = 31536000, **kwargs):
"""
Args:
cache_max_age: Cache TTL in seconds (default: 1 year)
"""
super().__init__(*args, **kwargs)
self.cache_max_age = cache_max_age
def generate_etag(self, file_path: Path) -> str:
"""Generate ETag from file content hash."""
with open(file_path, "rb") as f:
content_hash = hashlib.md5(f.read()).hexdigest()
return f'"{content_hash}"'
def file_response(self, *args, **kwargs) -> Response:
"""Return file response with caching headers."""
response = super().file_response(*args, **kwargs)
# Add Cache-Control header
response.headers["Cache-Control"] = (
f"public, max-age={self.cache_max_age}, immutable"
)
# Add Vary header for compression compatibility
response.headers["Vary"] = "Accept-Encoding"
# Add ETag for validation (optional, uncomment if needed)
# file_path = kwargs.get("path")
# if file_path:
# response.headers["ETag"] = self.generate_etag(file_path)
return response# deployment/nginx/mcpgateway.conf
server {
listen 80;
server_name gateway.example.com;
# Static assets with aggressive caching
location /static/ {
alias /var/www/mcpgateway/static/;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
# Serve pre-compressed files if available
gzip_static on;
brotli_static on;
# Security headers
add_header X-Content-Type-Options "nosniff";
}
# Proxy API requests to FastAPI
location / {
proxy_pass http://127.0.0.1:4444;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
}# deployment/caddy/Caddyfile
gateway.example.com {
# Static assets with caching
handle /static/* {
root * /var/www/mcpgateway
file_server
header Cache-Control "public, max-age=31536000, immutable"
header Vary "Accept-Encoding"
header X-Content-Type-Options "nosniff"
encode gzip brotli
}
# Proxy API requests to FastAPI
reverse_proxy localhost:4444 {
transport http {
dial_timeout 600s
response_header_timeout 600s
}
}
}📋 Implementation Tasks
Phase 1: CachedStaticFiles Implementation ✅
-
Create CachedStaticFiles Class
- Create new file: mcpgateway/utils/cached_static_files.py
- Implement CachedStaticFiles extending StaticFiles
- Add cache_max_age parameter (default: 31536000 = 1 year)
- Override file_response() to add Cache-Control header
- Add Vary: Accept-Encoding header
- Add comprehensive docstring
-
Add ETag Support (Optional)
- Implement generate_etag() method using file hash
- Add ETag header to file responses
- Handle If-None-Match conditional requests
- Test 304 Not Modified responses
Phase 2: Update StaticFiles Mount ✅
-
Update main.py StaticFiles Configuration
- Import CachedStaticFiles in mcpgateway/main.py
- Replace StaticFiles with CachedStaticFiles (around line 4450)
- Set cache_max_age=31536000 (1 year for versioned assets)
- Add comment explaining caching strategy
-
Test Cache Headers
- Start dev server:
make dev - Test with curl:
curl -I http://localhost:8000/static/css/style.css - Verify Cache-Control header present
- Verify Vary header present
- Test in browser DevTools (Network tab)
- Start dev server:
Phase 3: Content Hashing/Versioning ✅
-
Implement Version Query Parameter
- Update Jinja2 template helpers to add version to static URLs
- Add version parameter:
/static/css/style.css?v=0.8.0 - Update all static asset references in templates
- Test cache busting when version changes
-
Alternative: Create Build Script for Content Hashing
- Create scripts/hash_static_assets.py
- Generate hashed filenames:
style.a1b2c3.cssfromstyle.css - Create manifest.json mapping original → hashed names
- Update template helpers to read manifest
- Document build process in Makefile
Phase 4: nginx Configuration ✅
-
Create nginx Reverse Proxy Config
- Create directory: deployment/nginx/
- Create deployment/nginx/mcpgateway.conf with static asset config
- Configure static file serving from disk
- Add Cache-Control headers (1 year TTL)
- Enable gzip_static and brotli_static
- Configure proxy for API requests
-
Add SSL/TLS Configuration
- Create deployment/nginx/mcpgateway-ssl.conf
- Add SSL certificate configuration
- Configure HTTPS redirect
- Add security headers (HSTS, CSP, X-Frame-Options)
-
Create nginx Documentation
- Create deployment/nginx/README.md
- Document installation steps
- Document configuration and customization
- Add testing instructions
- Add troubleshooting section
Phase 5: Caddy Configuration ✅
-
Create Caddy Reverse Proxy Config
- Create directory: deployment/caddy/
- Create deployment/caddy/Caddyfile with static asset config
- Configure static file serving with automatic compression
- Set Cache-Control headers
- Configure reverse proxy for API requests
- Enable automatic HTTPS
-
Create Caddy Documentation
- Create deployment/caddy/README.md
- Document installation steps (Caddy auto-installs certs)
- Document configuration options
- Add testing instructions
- Compare nginx vs Caddy features
Phase 6: Pre-compression ✅
-
Create Pre-compression Script
- Create scripts/precompress_static_assets.sh
- Find all CSS, JS files in static/ directory
- Generate .gz files with gzip -k9
- Generate .br files with brotli -k
- Make script executable:
chmod +x
-
Add Pre-compression to Build Process
- Add
precompresstarget to Makefile - Run pre-compression before container builds
- Document in build instructions
- Test nginx serves .br and .gz files automatically
- Add
Phase 7: CDN Integration Documentation ✅
-
Create CDN Setup Guide
- Create docs/deployment/cdn-setup.md
- Section: CloudFlare setup instructions
- Section: AWS CloudFront setup instructions
- Section: Fastly setup instructions
- Section: Cache-Control header strategy
- Section: Purging/invalidation procedures
-
Document CDN Configuration
- Document static asset URL patterns for CDN
- Document cache TTLs for different asset types
- Document cache invalidation strategies
- Add example CDN configurations
- Document cost/performance trade-offs
Phase 8: Testing ✅
-
Test Browser Caching
- First visit: Verify all assets downloaded (200 OK)
- Second visit: Verify assets cached (no requests or 304)
- Verify Cache-Control headers in DevTools
- Test cache busting with version change
-
Test nginx Configuration
- Deploy nginx config to test server
- Verify static assets served from nginx
- Verify compression headers (Content-Encoding)
- Verify Cache-Control headers
- Test pre-compressed file serving (.br, .gz)
-
Test Caddy Configuration
- Deploy Caddy config to test server
- Verify automatic HTTPS works
- Verify static asset compression
- Verify Cache-Control headers
- Test automatic certificate renewal
-
Performance Testing
- Measure page load time (first visit)
- Measure page load time (repeat visit with cache)
- Calculate improvement percentage (should be 80-95%)
- Measure bandwidth usage reduction
Phase 9: Documentation ✅
-
Update CLAUDE.md
- Add section on static asset caching
- Document CachedStaticFiles configuration
- Explain Cache-Control strategy
- Add reverse proxy deployment instructions
-
Update Deployment Docs
- Add static asset optimization section
- Document nginx and Caddy options
- Add CDN integration guide
- Document pre-compression process
Phase 10: Quality Assurance ✅
-
Code Quality
- Run
make autoflake isort blackto format code - Run
make flake8and fix any issues - Run
make pylintand address warnings - Pass
make verifychecks
- Run
-
Testing
- Add unit tests for CachedStaticFiles
- Verify all existing tests still pass
- Test in multiple browsers (Chrome, Firefox, Safari)
- Test with browser cache disabled for baseline
✅ Success Criteria
- Cache-Control headers present on all static assets (max-age=31536000)
- ETags generated and validated correctly (optional)
- Content hashing or versioning implemented for cache busting
- nginx reverse proxy config created and tested
- Caddy reverse proxy config created and tested
- Pre-compressed static assets generated (.gz, .br)
- CDN integration documented (CloudFlare, CloudFront, Fastly)
- Browser caching working (304 responses on repeat visits)
- Static asset load time reduced by 80-95% on repeat visits
- Server load reduced (static assets not served by Python app)
- Documentation complete and accurate
- No breaking changes in UI functionality
🏁 Definition of Done
- CachedStaticFiles class implemented with Cache-Control headers
- ETag generation working (optional)
- Content hashing or versioning implemented
- nginx configuration template created and tested
- Caddy configuration template created and tested
- Pre-compression script created and documented
- CDN integration guide written
- Static asset versioning/hashing implemented
- Browser testing confirms caching works (304 responses)
- Performance testing shows 80-95% improvement on repeat visits
- Documentation complete (nginx, Caddy, CDN guides)
- Code passes
make verifychecks - No regression in existing tests
- Ready for production deployment
📝 Additional Notes
🔹 Cache-Control Strategy:
- Immutable Assets (versioned/hashed):
Cache-Control: public, max-age=31536000, immutable - Fingerprinted Assets: 1 year cache, no revalidation needed (immutable flag)
- HTML Files:
Cache-Control: no-cache(always revalidate with server) - API Responses: Varies by endpoint, typically no-cache or short TTL (5-60s)
- immutable directive: Tells browser file will never change (requires versioning)
🔹 CDN Benefits:
- Global Edge Caching: Assets cached in 200+ locations worldwide
- Reduced Latency: Assets served from nearest edge location (10-50ms vs 200-500ms)
- Reduced Bandwidth Costs: 80-95% reduction in origin bandwidth usage
- DDoS Protection: CDN absorbs attack traffic, protects origin server
- Automatic Optimization: Image optimization, format conversion, Brotli compression
- Analytics: Detailed traffic and performance metrics
🔹 nginx vs Caddy:
- nginx: Industry standard, high performance, manual config, requires separate SSL certs
- Caddy: Automatic HTTPS (Let's Encrypt), simpler config, built-in Brotli, easier to maintain
- Both: Support HTTP/2, compression, static file serving, reverse proxy
- nginx: Better for complex configurations, microservices, load balancing
- Caddy: Better for simple deployments, automatic HTTPS, less maintenance
🔹 Static Asset Checklist:
- ✅ Long cache TTL (1 year for versioned assets)
- ✅ Versioning or content hashing for cache busting
- ✅ Pre-compression (gzip, Brotli) for faster delivery
- ✅ ETag for validation (optional, useful for non-versioned assets)
- ✅ Vary: Accept-Encoding header for compression compatibility
- ✅ CDN or reverse proxy serving (offload from Python app)
- ✅ Security headers (X-Content-Type-Options: nosniff)
🔹 Content Hashing vs Versioning:
- Content Hashing:
style.a1b2c3.cssbased on file content hash- Pros: Automatic, only changes when file changes
- Cons: Build step required, more complex deployment
- Version Parameter:
style.css?v=0.8.0using app version- Pros: Simple, no build step, works with existing files
- Cons: All assets cache-busted on version bump, not granular
- Recommendation: Start with version parameter, migrate to content hashing later
🔹 Pre-compression Benefits:
- Faster: No runtime compression overhead, CPU-free
- Better Compression: Can use max compression level (slow but one-time)
- Consistent: Same compressed size every time, predictable performance
- Nginx: Use
gzip_static on; brotli_static on;to serve pre-compressed files - Build Once: Compress during build, not for every request
🔗 Related Issues
- Part of Performance Optimization initiative
- Related to [Feature]🔐 Configurable Password Expiration with Forced Password Change on Login #1282 (Compression) - static assets should be pre-compressed
- Related to Epic 2 (HTTP/2) - better for loading many small assets with multiplexing
- Complements caching strategies (browser cache + CDN cache)