# Performance Tuning Guide for Gobstopper This comprehensive guide covers performance optimization strategies for Gobstopper applications, from development through production deployment. ## Table of Contents - [Performance Overview](#performance-overview) - [Rust Components](#rust-components) - [Template Engine Optimization](#template-engine-optimization) - [Static File Serving](#static-file-serving) - [Database Optimization](#database-optimization) - [Caching Strategies](#caching-strategies) - [Background Tasks](#background-tasks) - [Request/Response Optimization](#requestresponse-optimization) - [Granian Server Configuration](#granian-server-configuration) - [Monitoring & Profiling](#monitoring--profiling) - [Production Deployment](#production-deployment) - [Benchmarking Results](#benchmarking-results) ## Performance Overview Gobstopper is designed for high performance through: - **RSGI Protocol**: Native async Python interface optimized for speed - **Rust Components**: Optional Rust-powered router, templates, and static files - **Efficient Task Queue**: Background processing with DuckDB persistence - **Smart Caching**: Template and static file caching - **Minimal Overhead**: Lean middleware chain with optional features - **Lazy Loading**: Headers, query params, and body parsing only when accessed - **Pre-compilation**: Middleware chains and decoders built at registration time ### Validated Performance Metrics Real-world benchmarks with PostgreSQL backend: - **JSON Serialization**: 17,000+ RPS - **Plaintext Response**: 18,000+ RPS - **Single DB Query**: 14,000+ RPS - **Multiple Queries (20)**: 5,500+ RPS - **Database Updates (5)**: 7,700+ RPS ### Version 0.3.6 Performance Improvements The 0.3.6 release includes conservative internal optimizations providing **3-8% throughput improvement**: | Optimization | Impact | Workload | |-------------|--------|----------| | Rust URL Decoding | ~3-5% | Routes with dynamic parameters | | Lazy Header Access | ~2-3% | Endpoints not using headers | | msgspec Decoder Caching | ~2-3% | JSON endpoints with msgspec models | | Lazy Query Parsing | ~1-2% | Routes without query params | | **Combined Effect** | **3-8%** | Typical web applications | These optimizations are **automatic** - no code changes required to benefit from them! The gains are modest but measurable, and more importantly, these changes maintain code simplicity and debuggability. ## Rust Components Gobstopper includes optional Rust components that provide significant performance improvements. These are automatically detected and used when available. ### Rust Router The Rust router provides 2-3x faster path matching compared to the Python implementation. **Installation:** ```bash # Install with Rust components cd rust/gobstopper_core_rs cargo build --release # Or install from project root uv sync --extra rust ``` **Verification:** ```python from gobstopper import Gobstopper app = Gobstopper(__name__) # Check if Rust router is active if hasattr(app, '_use_rust_router') and app._use_rust_router: print("✅ Using Rust router") else: print("⚠️ Using Python router") ``` **Performance Impact:** - Path matching: 2-3x faster - Parameter extraction: 1.5-2x faster - Route registration: Similar to Python - Best for: Applications with many routes (50+) ### Rust Template Engine The Rust template engine (Tera) is compatible with Jinja2 templates and offers: - 3-5x faster rendering for large templates - Lower memory usage - Built-in template caching **Setup:** ```python from gobstopper import Gobstopper app = Gobstopper(__name__, use_rust_templates=True) # Verify Rust templates are active if app.template_engine and 'Rust' in str(type(app.template_engine)): print("✅ Using Rust templates") ``` **When to Use:** - Complex templates with many loops/conditionals - High-traffic template rendering - Applications serving many concurrent users - Memory-constrained environments **Limitations:** - Some advanced Jinja2 features may not be available - Custom filters must be registered differently - Template debugging is less detailed ### Rust Static File Middleware Optimized static file serving with better performance characteristics: ```python from gobstopper.middleware import RustStaticFileMiddleware app = Gobstopper(__name__) app.add_middleware(RustStaticFileMiddleware( directory='./static', url_prefix='/static', cache_max_age=31536000 # 1 year for hashed assets )) ``` **Benefits:** - 2-4x faster file serving - Better handling of concurrent requests - Lower memory footprint - Automatic content type detection ## Template Engine Optimization ### Template Caching Both Jinja2 and Tera engines cache compiled templates by default: ```python from gobstopper import Gobstopper app = Gobstopper(__name__) # Jinja2 caching (default) app.template_engine.env.auto_reload = False # Disable reload in production app.template_engine.env.cache_size = 400 # Increase cache size # Monitor cache hits print(f"Cache info: {app.template_engine.env.cache}") ``` ### Template Best Practices **1. Minimize Template Complexity** ```html {% for user in users %} {% if user.active %} {% for post in user.posts %} {% if post.published %}
{{ post.title }}
{% endif %} {% endfor %} {% endif %} {% endfor %} {% for post in published_posts %}
{{ post.title }}
{% endfor %} ``` **2. Use Template Inheritance Efficiently** ```html {% block title %}Default{% endblock %} {% block head %}{% endblock %} {% block content %}{% endblock %} {% extends "base.html" %} {% block title %}{{ page_title }}{% endblock %} {% block content %} {{ content | safe }} {% endblock %} ``` **3. Avoid Heavy Filters in Templates** ```python # ❌ Slow: Complex filter in template @app.template_filter('format_markdown') def format_markdown(text): return markdown.markdown(text, extensions=['tables', 'fenced_code']) # ✅ Fast: Pre-process in view @app.get('/post/') async def show_post(request, id: int): post = await get_post(id) post.html_content = markdown.markdown(post.content) # Pre-process return await app.render_template('post.html', post=post) ``` ### Template Streaming For large pages, use streaming templates to improve perceived performance: ```python from gobstopper.http.response import StreamResponse @app.get('/large-report') async def large_report(request): async def generate(): # Yield header immediately yield await app.render_template_partial('header.html') # Stream data as it becomes available async for chunk in get_report_data(): yield await app.render_template_partial('row.html', data=chunk) # Yield footer yield await app.render_template_partial('footer.html') return StreamResponse(generate(), content_type='text/html') ``` ## Static File Serving ### Production Static File Strategy **Use a CDN or dedicated static server in production:** ```python # Development: Serve locally if os.getenv('ENV') != 'production': from gobstopper.middleware import RustStaticFileMiddleware app.add_middleware(RustStaticFileMiddleware( directory='./static', url_prefix='/static' )) else: # Production: Use CDN URLs app.static_url_prefix = 'https://cdn.example.com/static' ``` ### Asset Optimization **1. File Compression** Pre-compress static assets: ```bash # Compress CSS/JS with gzip and brotli find static -type f \( -name "*.css" -o -name "*.js" \) -exec gzip -k {} \; find static -type f \( -name "*.css" -o -name "*.js" \) -exec brotli {} \; ``` Configure middleware to serve pre-compressed files: ```python from gobstopper.middleware import RustStaticFileMiddleware app.add_middleware(RustStaticFileMiddleware( directory='./static', url_prefix='/static', precompressed=True # Serve .br and .gz files if available )) ``` **2. Asset Versioning** Use content hashing for cache busting: ```python import hashlib from pathlib import Path def hash_static_file(filepath: str) -> str: """Generate hash for static file.""" with open(filepath, 'rb') as f: return hashlib.md5(f.read()).hexdigest()[:8] # Build asset manifest static_dir = Path('./static') assets = {} for file in static_dir.rglob('*'): if file.is_file(): rel_path = str(file.relative_to(static_dir)) file_hash = hash_static_file(file) name, ext = rel_path.rsplit('.', 1) hashed_name = f"{name}.{file_hash}.{ext}" assets[rel_path] = hashed_name # Use in templates @app.context_processor async def inject_assets(): return {'asset': lambda path: f"/static/{assets.get(path, path)}"} ``` Template usage: ```html ``` **3. Cache Headers** Set appropriate cache headers: ```python from gobstopper.middleware import RustStaticFileMiddleware app.add_middleware(RustStaticFileMiddleware( directory='./static', url_prefix='/static', cache_max_age=31536000, # 1 year for versioned assets immutable=True # Assets never change )) ``` ## Database Optimization ### Connection Pooling Use connection pools for database access: ```python import asyncpg from contextlib import asynccontextmanager class DatabasePool: def __init__(self, dsn: str, min_size: int = 10, max_size: int = 20): self.dsn = dsn self.min_size = min_size self.max_size = max_size self.pool = None async def connect(self): self.pool = await asyncpg.create_pool( self.dsn, min_size=self.min_size, max_size=self.max_size, command_timeout=60 ) async def close(self): if self.pool: await self.pool.close() @asynccontextmanager async def acquire(self): async with self.pool.acquire() as conn: yield conn # Application setup db = DatabasePool('postgresql://user:pass@localhost/db') @app.on_startup async def startup(): await db.connect() @app.on_shutdown async def shutdown(): await db.close() # Usage in routes @app.get('/users/') async def get_user(request, id: int): async with db.acquire() as conn: user = await conn.fetchrow('SELECT * FROM users WHERE id = $1', id) return JSONResponse(dict(user)) ``` ### Query Optimization **1. Use Prepared Statements** ```python # ❌ Slow: New query each time async def get_user(user_id: int): async with db.acquire() as conn: return await conn.fetchrow( f'SELECT * FROM users WHERE id = {user_id}' # SQL injection risk! ) # ✅ Fast: Prepared statement async def get_user(user_id: int): async with db.acquire() as conn: return await conn.fetchrow( 'SELECT * FROM users WHERE id = $1', user_id # Safe and cached ) ``` **2. Batch Queries** ```python # ❌ Slow: N+1 queries async def get_users_with_posts(user_ids: list[int]): users = [] async with db.acquire() as conn: for user_id in user_ids: user = await conn.fetchrow('SELECT * FROM users WHERE id = $1', user_id) posts = await conn.fetch('SELECT * FROM posts WHERE user_id = $1', user_id) users.append({'user': user, 'posts': posts}) return users # ✅ Fast: Batch query with JOIN async def get_users_with_posts(user_ids: list[int]): async with db.acquire() as conn: rows = await conn.fetch(''' SELECT u.*, p.id as post_id, p.title, p.content FROM users u LEFT JOIN posts p ON p.user_id = u.id WHERE u.id = ANY($1) ''', user_ids) # Group results in Python return group_by_user(rows) ``` **3. Use Indexes** ```sql -- Create indexes for frequently queried columns CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_posts_user_id ON posts(user_id); CREATE INDEX idx_posts_created_at ON posts(created_at DESC); -- Composite indexes for multi-column queries CREATE INDEX idx_posts_user_status ON posts(user_id, status); ``` ### Database Query Caching Cache expensive query results: ```python from functools import lru_cache import asyncio # Simple in-memory cache with TTL cache = {} cache_timestamps = {} CACHE_TTL = 60 # 60 seconds async def cached_query(key: str, query_func, ttl: int = CACHE_TTL): """Cache query results with TTL.""" now = asyncio.get_event_loop().time() if key in cache: if now - cache_timestamps[key] < ttl: return cache[key] result = await query_func() cache[key] = result cache_timestamps[key] = now return result # Usage @app.get('/stats/users') async def user_stats(request): async def get_stats(): async with db.acquire() as conn: return await conn.fetchrow('SELECT COUNT(*) as total FROM users') stats = await cached_query('user_stats', get_stats, ttl=300) return JSONResponse(dict(stats)) ``` ## Caching Strategies ### Response Caching Cache entire responses for read-heavy endpoints: ```python from functools import wraps import hashlib import json response_cache = {} def cache_response(ttl: int = 60): """Decorator to cache responses.""" def decorator(func): @wraps(func) async def wrapper(request, *args, **kwargs): # Create cache key from route and args cache_key = hashlib.md5( f"{request.path}:{json.dumps(kwargs)}".encode() ).hexdigest() # Check cache if cache_key in response_cache: cached, timestamp = response_cache[cache_key] if asyncio.get_event_loop().time() - timestamp < ttl: return cached # Generate response response = await func(request, *args, **kwargs) # Cache it response_cache[cache_key] = (response, asyncio.get_event_loop().time()) return response return wrapper return decorator # Usage @app.get('/api/popular-posts') @cache_response(ttl=300) # Cache for 5 minutes async def popular_posts(request): posts = await get_popular_posts() return JSONResponse(posts) ``` ### Redis Caching For distributed caching across multiple servers: ```python import aioredis import json class RedisCache: def __init__(self, redis_url: str): self.redis_url = redis_url self.redis = None async def connect(self): self.redis = await aioredis.from_url(self.redis_url) async def get(self, key: str): value = await self.redis.get(key) return json.loads(value) if value else None async def set(self, key: str, value, ttl: int = 60): await self.redis.setex(key, ttl, json.dumps(value)) async def delete(self, key: str): await self.redis.delete(key) # Setup cache = RedisCache('redis://localhost:6379/0') @app.on_startup async def startup(): await cache.connect() # Usage @app.get('/api/user/') async def get_user(request, id: int): cache_key = f"user:{id}" # Try cache first user = await cache.get(cache_key) if user: return JSONResponse(user) # Fetch from database async with db.acquire() as conn: user = await conn.fetchrow('SELECT * FROM users WHERE id = $1', id) user = dict(user) # Cache for 5 minutes await cache.set(cache_key, user, ttl=300) return JSONResponse(user) ``` ## Background Tasks ### Task Queue Optimization Configure the task queue for optimal performance: ```python from gobstopper import Gobstopper from pathlib import Path app = Gobstopper(__name__) # Configure task storage app.task_storage.cleanup_interval = 3600 # Cleanup every hour app.task_storage.retention_days = 7 # Keep completed tasks for 7 days # Start workers with concurrency @app.on_startup async def startup(): # Start multiple workers for CPU-intensive tasks await app.start_workers( num_workers=4, # 4 concurrent workers categories=['processing', 'heavy'] ) # Start dedicated worker for I/O tasks await app.start_workers( num_workers=8, # More workers for I/O categories=['email', 'api'] ) ``` ### Task Priorities Use priorities for critical tasks: ```python from gobstopper.tasks.models import TaskPriority # High priority task @app.task(name='send_verification_email', category='email', priority=TaskPriority.HIGH) async def send_verification_email(user_id: int, email: str): await send_email(email, 'Verify your account', '...') # Normal priority task @app.task(name='generate_report', category='processing', priority=TaskPriority.NORMAL) async def generate_report(report_id: int): await generate_pdf_report(report_id) # Low priority task @app.task(name='cleanup_old_data', category='maintenance', priority=TaskPriority.LOW) async def cleanup_old_data(): await delete_old_records() ``` ### Batch Processing Process multiple items in batches: ```python @app.task(name='process_batch', category='processing') async def process_batch(item_ids: list[int]): """Process multiple items at once.""" async with db.acquire() as conn: # Fetch all items in one query items = await conn.fetch( 'SELECT * FROM items WHERE id = ANY($1)', item_ids ) # Process batch for item in items: await process_item(item) # Queue batches instead of individual items @app.post('/api/process') async def queue_processing(request): data = await request.json() item_ids = data['item_ids'] # Split into batches of 100 batch_size = 100 for i in range(0, len(item_ids), batch_size): batch = item_ids[i:i + batch_size] await app.queue_task('process_batch', item_ids=batch) return JSONResponse({'queued': len(item_ids)}) ``` ## Request/Response Optimization ### Automatic Optimizations (v0.3.6+) Gobstopper 0.3.6 includes several automatic optimizations that improve performance without any code changes: #### Lazy Header Access Headers are no longer parsed and lowercased on every request. Instead, they're computed only when accessed: ```python @app.get('/api/data') async def get_data(request): # If you don't access request.headers, no processing occurs # This saves ~2-3% overhead for simple endpoints return JSONResponse({'data': 'value'}) @app.get('/api/auth') async def check_auth(request): # Headers are parsed only when needed token = request.headers.get('authorization') # Lazy computation happens here return JSONResponse({'authenticated': bool(token)}) ``` **Best Practice**: Only access headers when necessary to maximize this optimization. #### msgspec Decoder Caching JSON decoders for `msgspec.Struct` models are cached on the model class: ```python import msgspec class User(msgspec.Struct): name: str email: str age: int @app.post('/api/users') async def create_user(request: Request, user: User): # First request: decoder created and cached on User class # Subsequent requests: cached decoder reused (~2-3% faster) return JSONResponse({'created': user.name}) ``` **No configuration needed** - caching happens automatically. #### Lazy Query String Parsing Query parameters are parsed only when `request.args` is accessed: ```python @app.get('/api/search') async def search(request): # If you don't use query params, parsing is skipped # For requests without query strings, this is a free optimization query = request.args.get('q', [''])[0] if request.args else '' return JSONResponse({'query': query}) ``` #### Rust-Level URL Decoding URL decoding moved to Rust router for native-speed processing: ```python # URL with encoded characters: /api/users/John%20Doe @app.get('/api/users/') async def get_user(request, name: str): # 'name' is decoded at Rust level (3-5% faster) # You receive: name = "John Doe" return JSONResponse({'user': name}) ``` ### JSON Serialization Use fast JSON serializers: ```python # Option 1: msgspec (fastest) import msgspec @app.get('/api/data') async def get_data(request): data = {'items': [...]} # Large dataset json_bytes = msgspec.json.encode(data) return Response(json_bytes, content_type='application/json') # Option 2: orjson (very fast) import orjson @app.get('/api/data') async def get_data(request): data = {'items': [...]} json_bytes = orjson.dumps(data) return Response(json_bytes, content_type='application/json') ``` ### Response Compression Enable compression for text responses: ```python from gobstopper.middleware import CompressionMiddleware app.add_middleware(CompressionMiddleware( minimum_size=1000, # Only compress responses > 1KB compression_level=6 # Balance between speed and size )) ``` ### Request Body Parsing Parse request bodies efficiently: ```python # ❌ Slow: Multiple awaits @app.post('/api/users') async def create_user(request): json_data = await request.json() form_data = await request.get_form() # Never used # ... # ✅ Fast: Parse only what you need @app.post('/api/users') async def create_user(request): json_data = await request.json() # Only parse JSON # ... ``` ## Granian Server Configuration Optimize Granian server settings for production: ```bash # Development granian --interface rsgi --reload example_app:app # Production: Multiple workers granian --interface rsgi \ --workers 4 \ --threads 2 \ --blocking-threads 2 \ --backlog 2048 \ example_app:app # Production: Optimized for high concurrency granian --interface rsgi \ --workers $(nproc) \ --threads 4 \ --blocking-threads 4 \ --backlog 4096 \ --http1-buffer-size 8192 \ --http1-keep-alive \ --http2-max-concurrent-streams 1000 \ example_app:app ``` **Configuration guidelines:** - **Workers**: `num_cpu_cores` or `num_cpu_cores + 1` - **Threads**: 2-4 per worker for mixed I/O and CPU work - **Blocking Threads**: Match threads for blocking operations - **Backlog**: 2048-4096 for high-traffic sites - **Keep-Alive**: Enable for persistent connections ### Worker Strategy Choose worker strategy based on workload: ```bash # CPU-intensive: More workers, fewer threads granian --interface rsgi --workers 8 --threads 1 app:app # I/O-intensive: Fewer workers, more threads granian --interface rsgi --workers 2 --threads 8 app:app # Mixed workload: Balanced (recommended) granian --interface rsgi --workers 4 --threads 2 app:app ``` ## Monitoring & Profiling ### Application Profiling Profile your application to identify bottlenecks: ```python import cProfile import pstats from pstats import SortKey def profile_route(): """Profile a specific route.""" profiler = cProfile.Profile() profiler.enable() # Your code here response = await some_route(request) profiler.disable() stats = pstats.Stats(profiler) stats.sort_stats(SortKey.TIME) stats.print_stats(20) # Top 20 functions # Or use middleware for profiling @app.middleware async def profiling_middleware(request, call_next): if request.path.startswith('/profile/'): profiler = cProfile.Profile() profiler.enable() response = await call_next(request) profiler.disable() # Save stats profiler.dump_stats(f'profile_{request.path.replace("/", "_")}.prof') return response return await call_next(request) ``` ### Performance Metrics Track key metrics: ```python import time from collections import defaultdict metrics = { 'request_count': defaultdict(int), 'request_duration': defaultdict(list), 'error_count': defaultdict(int) } @app.middleware async def metrics_middleware(request, call_next): start_time = time.time() try: response = await call_next(request) metrics['request_count'][request.path] += 1 duration = time.time() - start_time metrics['request_duration'][request.path].append(duration) return response except Exception as e: metrics['error_count'][request.path] += 1 raise # Expose metrics endpoint @app.get('/metrics') async def get_metrics(request): stats = {} for path, durations in metrics['request_duration'].items(): stats[path] = { 'count': metrics['request_count'][path], 'avg_duration': sum(durations) / len(durations), 'max_duration': max(durations), 'errors': metrics['error_count'][path] } return JSONResponse(stats) ``` ### Load Testing Use load testing tools to validate performance: ```bash # wrk: HTTP benchmarking wrk -t12 -c400 -d30s http://localhost:8000/api/data # Apache Bench ab -n 10000 -c 100 http://localhost:8000/ # Locust: Python-based load testing locust -f locustfile.py --host=http://localhost:8000 ``` Example Locust file: ```python from locust import HttpUser, task, between class WebsiteUser(HttpUser): wait_time = between(1, 3) @task(3) def get_homepage(self): self.client.get("/") @task(2) def get_api_data(self): self.client.get("/api/data") @task(1) def post_data(self): self.client.post("/api/users", json={ "name": "Test User", "email": "test@example.com" }) ``` ## Production Deployment ### Optimization Checklist - [ ] Enable Rust components (router, templates, static files) - [ ] Configure database connection pooling - [ ] Set up Redis for distributed caching - [ ] Enable response compression - [ ] Use CDN for static assets - [ ] Configure proper cache headers - [ ] Enable HTTP/2 in reverse proxy - [ ] Set up multiple Granian workers - [ ] Implement response caching for read-heavy endpoints - [ ] Profile and optimize slow queries - [ ] Monitor application metrics - [ ] Set up load balancing - [ ] Enable database query result caching - [ ] Optimize template rendering - [ ] Use background tasks for heavy operations ### Deployment Architecture Recommended production setup: ``` Internet ↓ Load Balancer (AWS ELB, Nginx) ↓ ┌─────────────────────────────────┐ │ Reverse Proxy (Nginx/Caddy) │ │ - TLS termination │ │ - Static file serving │ │ - Gzip/Brotli compression │ │ - Rate limiting │ └─────────────────────────────────┘ ↓ ┌─────────────────────────────────┐ │ Granian Workers (4-8) │ │ - Gobstopper application │ │ - Rust components enabled │ │ - Connection pooling │ └─────────────────────────────────┘ ↓ ↓ ┌──────────────┐ ┌──────────────┐ │ PostgreSQL │ │ Redis Cache │ │ (Primary) │ │ │ └──────────────┘ └──────────────┘ ``` ### Nginx Configuration ```nginx upstream gobstopper_app { least_conn; server 127.0.0.1:8000; server 127.0.0.1:8001; server 127.0.0.1:8002; server 127.0.0.1:8003; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # Gzip compression gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1000; # Static files location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; } # Application location / { proxy_pass http://gobstopper_app; 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 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; # Buffer settings proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; } } ``` ## Benchmarking Results ### Test Environment - **Hardware**: 8 CPU cores, 16GB RAM - **Database**: PostgreSQL 15 with connection pool (20 connections) - **Configuration**: 4 Granian workers, Rust components enabled ### Results | Test Type | Requests/sec | Latency (avg) | Latency (p99) | |-----------|-------------|---------------|---------------| | Plaintext | 18,241 | 2.1ms | 5.2ms | | JSON | 17,486 | 2.3ms | 6.1ms | | Single Query | 14,203 | 2.8ms | 8.4ms | | Multiple Queries (20) | 5,523 | 7.2ms | 18.1ms | | Updates (5) | 7,741 | 5.1ms | 14.2ms | | Templates | 12,004 | 3.3ms | 9.7ms | ### Comparison with Other Frameworks Gobstopper performance is competitive with major Python async frameworks: - **FastAPI**: Similar performance for JSON serialization - **Starlette**: Comparable baseline performance - **BlackSheep**: Similar template rendering speed - **Sanic**: Gobstopper typically 10-15% faster with Rust components ### Continuous Performance Testing Set up automated benchmarks: ```bash #!/bin/bash # benchmark.sh - Run performance tests echo "Running Gobstopper benchmarks..." # Start application granian --interface rsgi --workers 4 benchmark_simple:app & APP_PID=$! sleep 2 # Run tests wrk -t4 -c100 -d30s http://localhost:8000/json > results_json.txt wrk -t4 -c100 -d30s http://localhost:8000/db > results_db.txt # Stop application kill $APP_PID # Parse and compare results python compare_benchmarks.py results_json.txt results_db.txt ``` ## Conclusion Gobstopper provides excellent performance out of the box, but following these optimization strategies can push performance even higher: 1. **Enable Rust components** for 2-4x performance gains 2. **Use connection pooling** for database access 3. **Implement caching** at multiple levels 4. **Optimize database queries** with proper indexes 5. **Configure Granian** with appropriate worker settings 6. **Monitor and profile** to identify bottlenecks 7. **Use CDN** for static assets in production For most applications, these optimizations will achieve 15,000+ RPS with sub-5ms latency.