Use when diagnosing a slow HTTP endpoint or high-latency service — profiling, adding an application-level cache, offloading CPU-bound work to threads or workers, or defining a latency budget. For slow queries or missing indexes, use database-design or postgresql.
Installs into .claude/skills of the current project.
Are you the author of Performance?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kid-sid-performance-claude-spellbook)
---
name: performance
description: Use when diagnosing a slow HTTP endpoint or high-latency service — profiling, adding an application-level cache, offloading CPU-bound work to threads or workers, or defining a latency budget. For slow queries or missing indexes, use database-design or postgresql.
---
# Performance
A structured guide to profiling, caching, database optimization, async patterns, and performance budgets for production services.
## When to Activate
- Profiling a slow endpoint or service
- Implementing a caching layer (in-process, Redis, or HTTP)
- Optimizing a database query or fixing N+1 problems
- Setting a performance budget for an API endpoint
- Reducing memory usage or GC pressure
- Choosing between sync and async patterns for a workload
---
## Profiling
### When to Profile
- Profile before optimizing — never guess where the bottleneck is
- **CPU profiling** — where is time spent (function call time)?
- **Memory profiling** — what objects are consuming heap space?
- **I/O profiling** — what is blocking on disk or network?
### Python — cProfile + snakeviz
```python
import cProfile
import pstats
import io
pr = cProfile.Profile()
pr.enable()
result = my_slow_function()
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
ps.print_stats(20) # top 20 slowest functions
print(s.getvalue())
# Profile a whole script from the command line:
# python -m cProfile -o output.prof script.py
# snakeviz output.prof # opens interactive flame graph in browser
```
Memory profiling with `memory_profiler`:
```python
# pip install memory-profiler
from memory_profiler import profile
@profile
def my_function():
# annotated line-by-line memory usage
data = [x for x in range(10_000_000)]
return data
```
### TypeScript/Node.js — clinic.js + 0x
```bash
# CPU flame graph
npx 0x -- node dist/server.js
# Opens a generated .html flame graph in the browser
# Heap snapshot + event loop lag
npx clinic doctor -- node dist/server.js
# CPU flame graph via clinic
npx clinic flame -- node dist/server.js
# Async waterfall / I/O bottlenecks
npx clinic bubbleprof -- node dist/server.js
```
### Go — pprof
```go
import (
"net/http"
_ "net/http/pprof" // side-effect import registers /debug/pprof handlers
)
// In main(), run alongside your app server:
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
```
```bash
# CPU profile (30-second sample)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Memory (heap) profile
go tool pprof http://localhost:6060/debug/pprof/heap
# In the pprof interactive prompt:
# (pprof) top10 — top 10 functions by CPU or memory
# (pprof) web — open flame graph in browser (requires graphviz)
# (pprof) list FuncName — annotated source with per-line costs
```
### Reading Flame Graphs
- **X-axis** — time (box width = proportion of total execution time)
- **Y-axis** — call stack depth (parent calls children above it)
- **Wide flat boxes near the top** — hot code paths; primary optimization targets
- **Long stacks with narrow top boxes** — deep recursion; usually not a problem
- **Plateaus** — the widest boxes in the middle of a stack often hide the real work
---
## Caching Strategies
### Strategy Comparison
| Strategy | Scope | Latency | Consistency | Best For |
|----------|-------|---------|-------------|----------|
| In-process LRU | Single instance | ~nanoseconds | Per-instance (inconsistent across replicas) | Immutable lookups, config, computed values |
| Distributed (Redis) | All instances | ~1 ms | Eventually consistent | Session state, rate limits, shared counters |
| HTTP cache (CDN/browser) | Client + CDN | ~0 ms on hit | TTL-based | Public read-heavy content, static assets |
### Cache-Aside Pattern (most common)
```python
def get_user(user_id: str) -> User:
# 1. Check cache first
cached = redis.get(f"user:{user_id}")
if cached:
return User.from_json(cached)
# 2. Cache miss — fetch from DB
user = db.query(User).filter(User.id == user_id).first()
# 3. Populate cache with TTL
redis.setex(f"user:{user_id}", 300, user.to_json()) # 5 min TTL
return user
```
### Caching Pattern Comparison
| Pattern | Description | Consistency | Use When |
|---------|-------------|-------------|----------|
| Cache-aside | App manages cache reads and writes | Eventual | General purpose (most cases) |
| Read-through | Cache fetches from DB automatically on miss | Eventual | Simplify application read code |
| Write-through | Write to cache and DB synchronously | Strong | Read-heavy workloads needing consistency |
| Write-behind | Write to cache, async write to DB | Eventual | Write-heavy workloads that can accept risk |
### Cache Invalidation
- **TTL (time-to-live)** — simplest; accept stale data up to TTL duration
- **Event-driven** — invalidate on write (`redis.delete(f"user:{user_id}")` after UPDATE)
- **Write-through** — always write to both cache and DB; no stale data, but slower writes
- **Avoid** — invalidating cache on reads is an anti-pattern; adds latency to hot paths
### In-Process LRU Cache
```python
# Python
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_config(key: str) -> str:
return db.get_config(key)
```
```typescript
// TypeScript
import LRU from 'lru-cache';
const cache = new LRU<string, string>({ max: 1000, ttl: 1000 * 60 * 5 });
function getConfig(key: string): string {
if (cache.has(key)) return cache.get(key)!;
const value = db.getConfig(key);
cache.set(key, value);
return value;
}
```
```go
// Go
import "github.com/hashicorp/golang-lru/v2"
cache, _ := lru.New[string, string](1000)
func getConfig(key string) string {
if val, ok := cache.Get(key); ok {
return val
}
val := db.GetConfig(key)
cache.Add(key, val)
return val
}
```
---
## HTTP Cache Headers
| Header | Example Value | What It Controls |
|--------|--------------|-----------------|
| `Cache-Control` | `max-age=3600, s-maxage=86400` | Browser and CDN TTL |
| `ETag` | `"abc123"` | Version fingerprint for conditional requests |
| `Last-Modified` | `Wed, 15 Jan 2025 10:00:00 GMT` | Last modified time for conditional requests |
| `Vary` | `Accept-Encoding, Accept-Language` | Keys the cache on these request headers |
### Key `Cache-Control` Directives
| Directive | Meaning |
|-----------|---------|
| `max-age=N` | Browser caches for N seconds |
| `s-maxage=N` | CDN caches for N seconds (overrides `max-age` for CDN) |
| `no-cache` | Revalidate with server on every request (ETag/If-None-Match check) |
| `no-store` | Never cache (sensitive data) |
| `private` | Browser only — not stored by CDN |
| `stale-while-revalidate=N` | Serve stale while fetching fresh in background |
| `immutable` | Content will never change (pair with hash-based filenames) |
### Conditional Requests (ETag)
```http
# First request
GET /api/products/123
→ 200 OK
ETag: "v2-abc123"
Cache-Control: max-age=60
# After TTL expires — client sends ETag back
GET /api/products/123
If-None-Match: "v2-abc123"
→ 304 Not Modified (no response body — saves bandwidth)
# or, if product changed:
→ 200 OK
ETag: "v3-def456"
```
---
## Database N+1 Problem
### The Problem
```python
# BAD: N+1 — 1 query for orders + 1 query per order for its user
orders = db.query(Order).all() # 1 query
for order in orders:
print(order.user.name) # N queries (lazy load per order)
```
With 500 orders this emits 501 queries. Use `EXPLAIN ANALYZE` or ORM query logging to detect this in review.
### Fix Per ORM
**Python — SQLAlchemy**
```python
from sqlalchemy.orm import selectinload, joinedload
# selectinload: 2 queries total — 1 for orders, 1 IN query for all related users
orders = db.query(Order).options(selectinload(Order.user)).all()
# joinedload: 1 query with a JOIN (better for single related object)
orders = db.query(Order).options(joinedload(Order.user)).all()
```
**TypeScript — Prisma**
```typescript
// BAD
const orders = await prisma.order.findMany();
for (const order of orders) {
const user = await prisma.user.findUnique({ where: { id: order.userId } });
}
// GOOD — Prisma batches the related fetches automatically
const orders = await prisma.order.findMany({
include: { user: true },
});
```
**Go — GORM**
```go
var orders []Order
// BAD — N separate queries inside the loop
db.Find(&orders)
for i := range orders {
db.First(&orders[i].User, orders[i].UserID)
}
// GOOD — Preload issues a single IN query for all users
db.Preload("User").Find(&orders)
```
### Detecting N+1 in Practice
| Tool | How to Enable |
|------|--------------|
| SQLAlchemy | `echo=True` on `create_engine`, or use `sqlalchemy-query-counter` |
| Prisma | `log: ['query']` in `PrismaClient` constructor |
| GORM | `db.Debug()` or custom logger |
| Django ORM | `django-debug-toolbar` or `connection.queries` |
| General | `EXPLAIN ANALYZE SELECT ...` in psql to see sequential scans |
---
## Async Patterns
### I/O-Bound vs CPU-Bound
| Work Type | Python | TypeScript/Node.js | Go |
|-----------|--------|--------------------|----|
| I/O-bound (HTTP calls, DB) | `asyncio` / `async def` | `async/await` (native event loop) | goroutines (native) |
| CPU-bound (computation) | `ProcessPoolExecutor` (bypass GIL) | `worker_threads` module | goroutines (native, real parallelism) |
| Background jobs | Celery, RQ | BullMQ, Agenda | goroutines + channels |
### Python — asyncio for I/O-Bound Work
```python
import asyncio
import aiohttp
async def fetch_all(urls: list[str]) -> list[dict]:
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks) # concurrent, not parallel
async def fetch(session: aiohttp.ClientSession, url: str) -> dict:
async with session.get(url) as response:
return await response.json()
```
CPU-bound work in Python must use `ProcessPoolExecutor` to escape the GIL:
```python
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(data: list) -> int:
return sum(x ** 2 for x in data)
async def process_many(chunks: list[list]) -> list[int]:
loop = asyncio.get_event_loop()
with ProcessPoolExecutor() as pool:
results = await asyncio.gather(
*[loop.run_in_executor(pool, cpu_heavy, chunk) for chunk in chunks]
)
return results
```
### TypeScript/Node.js — Protect the Event Loop
```typescript
import fs from 'fs';
import { Worker, isMainThread, workerData, parentPort } from 'worker_threads';
// BAD: sync read blocks the event loop for all requests
const data = fs.readFileSync('large-file.json', 'utf8');
// GOOD: async I/O — yields control back to event loop
const data = await fs.promises.readFile('large-file.json', 'utf8');
// BAD: CPU-heavy work in the main thread stalls all requests
const result = heavyComputation(data);
// GOOD: offload CPU work to a worker thread
function runInWorker(payload: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: payload });
worker.on('message', resolve);
worker.on('error', reject);
});
}
```
### Go — Goroutines for Concurrency
```go
// Fan-out: fire N goroutines, collect with WaitGroup + channel
func fetchAll(urls []string) []Result {
results := make(chan Result, len(urls))
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
results <- Result{URL: u, Err: err, Body: readBody(resp)}
}(url)
}
wg.Wait()
close(results)
var out []Result
for r := range results {
out = append(out, r)
}
return out
}
```
---
## Performance Budgets
### Deriving a Budget from SLOs
- If the SLO is **p99 < 500 ms**, the internal service call budget is ~200 ms (leave headroom for network, serialization, retries)
- Decompose latency: `total = DB + cache + downstream API + serialization + middleware`
- Assign each component a share; the tightest constraint sets the overall shape
### Endpoint Budget Reference
| Endpoint Category | p50 Target | p99 Target |
|-------------------|-----------|-----------|
| Read-only lookups | < 50 ms | < 200 ms |
| Search / aggregation | < 200 ms | < 1 s |
| Write operations | < 100 ms | < 500 ms |
| Background jobs | N/A | N/A (use queue depth + processing lag metrics) |
### CI Regression Detection
```yaml
# k6 threshold example — fails the PR if p99 regresses
import http from 'k6/http';
import { check } from 'k6';
export const options = {
thresholds: {
http_req_duration: ['p(99)<500'], // fail if p99 > 500ms
},
};
export default function () {
const res = http.get('http://localhost:3000/api/users/1');
check(res, { 'status 200': (r) => r.status === 200 });
}
```
Run this in CI on every PR:
```bash
k6 run --vus 50 --duration 30s load-test.js
# Exit code non-zero if any threshold is breached
```
Fail the build if p99 degrades more than 20% from the baseline captured on `main`.
> See also: `database-design`, `observability`, `performance-testing`
---
## Red Flags
- **Optimizing before profiling** — intuition targets the wrong 5% of runtime; always profile with representative load before touching any code
- **Profiling with 1K rows when production has 10M** — hotspots at small scale vanish or invert at large scale; profile with production-representative data volume
- **Blanket eager loading to fix N+1** — fetching every relationship on every query loads data you never use; apply `selectinload`/`joinedload` surgically to proven hotspots
- **In-process LRU cache across forked workers** — forked processes maintain separate memory spaces; a cache write in one worker is invisible to others; use Redis for cross-process caching
- **Async for CPU-bound work** — Python asyncio and Node.js event loops don't parallelize CPU; CPU-bound work blocks the loop; offload to `ProcessPoolExecutor` or a task queue
- **ETags set but `If-None-Match` not handled server-side** — setting ETag without handling conditional requests means clients never get 304; implement both sides of the exchange
- **Mean latency as the primary metric** — mean hides tail problems; always track p95 and p99; the slowest 1% of requests represents the worst user experience
## Checklist
- [ ] Profiled before optimizing — no premature optimization
- [ ] Flame graph or profile output captured to identify the actual bottleneck
- [ ] N+1 queries detected with `EXPLAIN ANALYZE` or ORM query logging
- [ ] Eager loading configured for all related-entity fetches
- [ ] Cache layer added for hot read paths (in-process for single-instance, Redis for distributed)
- [ ] Cache keys include version or tenant identifier to prevent cross-user data leaks
- [ ] `Cache-Control` headers set for all public API responses
- [ ] ETags implemented for cacheable resources (304 responses save bandwidth)
- [ ] `async/await` or equivalent used for all I/O-bound operations
- [ ] CPU-bound work offloaded to worker processes or threads
- [ ] Performance budget defined per endpoint category and documented
- [ ] Baseline p50/p95/p99 measured before and after changes