Best Practices
Performance Optimization
Maximize API speed and efficiency with caching, pagination, rate limiting, and query optimization techniques.
Performance Overview
Response Time
Minimize latency between request and response for better user experience.
Excellent:< 100ms
Good:100-300ms
Acceptable:300-1000ms
Slow:> 1000ms
Throughput
Number of requests your API can handle per second.
Scale horizontally with load balancers
Use connection pooling
Optimize database queries
Implement caching strategies
Payload Size
Keep response sizes minimal to reduce transfer time and bandwidth.
Use field selection/filtering
Enable GZIP compression
Paginate large datasets
Remove unnecessary fields
Resource Usage
Optimize CPU, memory, and network usage for cost efficiency.
Monitor resource consumption
Use async/non-blocking operations
Implement connection limits
Clean up idle connections
Caching Strategies
HTTP Caching
Cache-Control Headers
// Cache for 1 hour (public resources) Cache-Control: public, max-age=3600 // Cache for 5 minutes (private user data) Cache-Control: private, max-age=300 // No caching (sensitive data) Cache-Control: no-cache, no-store, must-revalidate // Cache but validate with server (ETags) Cache-Control: no-cache ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
ETag (Entity Tag) Validation
// Server response with ETag
HTTP/1.1 200 OK
ETag: "686897696a7c876b7e"
Content-Type: application/json
{ "id": 123, "name": "John Doe" }
// Client subsequent request
GET /users/123
If-None-Match: "686897696a7c876b7e"
// Server response if not modified
HTTP/1.1 304 Not Modified
ETag: "686897696a7c876b7e"
// No response body sent (saves bandwidth)
// Implementation example
function getUserWithETag(userId) {
const user = db.users.findById(userId);
const etag = generateETag(user);
if (req.headers['if-none-match'] === etag) {
return res.status(304).send();
}
return res
.set('ETag', etag)
.set('Cache-Control', 'no-cache')
.json(user);
}
function generateETag(data) {
return crypto
.createHash('md5')
.update(JSON.stringify(data))
.digest('hex');
}Last-Modified Header
// Server response
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Jan 2024 10:30:00 GMT
Content-Type: application/json
{ "id": 123, "name": "John Doe" }
// Client subsequent request
GET /users/123
If-Modified-Since: Wed, 15 Jan 2024 10:30:00 GMT
// Server response if not modified
HTTP/1.1 304 Not ModifiedServer-Side Caching
In-Memory Cache (Redis/Memcached)
// Redis cache implementation
import Redis from 'ioredis';
const redis = new Redis({
host: 'localhost',
port: 6379
});
async function getUsers() {
const cacheKey = 'users:all';
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from database
const users = await db.users.findAll();
// Store in cache for 5 minutes
await redis.setex(
cacheKey,
300, // TTL in seconds
JSON.stringify(users)
);
return users;
}
// Cache invalidation on update
async function updateUser(userId, updates) {
const user = await db.users.update(userId, updates);
// Invalidate related cache keys
await redis.del('users:all');
await redis.del(`users:${userId}`);
return user;
}Query Result Caching
// Cache expensive queries
async function getProductsByCategory(category, filters) {
const cacheKey = `products:${category}:${JSON.stringify(filters)}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Expensive database query
const products = await db.products
.where({ category })
.filter(filters)
.sort('-created_at')
.limit(100);
// Cache for 10 minutes
await redis.setex(cacheKey, 600, JSON.stringify(products));
return products;
}Cache Best Practices
Cache static data: Configuration, reference data, rarely changing content
Appropriate TTL: Balance freshness vs performance (5-60 minutes typical)
Invalidate on writes: Clear cache when data changes
Use cache keys wisely: Include version, filters, user context
Monitor cache hit rates: Aim for 80%+ hit rate
Pagination Strategies
1. Offset-Based Pagination (Simple)
// Request
GET /users?page=2&per_page=20
// Response
{
"data": [...], // 20 users
"meta": {
"page": 2,
"per_page": 20,
"total": 156,
"total_pages": 8
},
"links": {
"first": "/users?page=1&per_page=20",
"prev": "/users?page=1&per_page=20",
"next": "/users?page=3&per_page=20",
"last": "/users?page=8&per_page=20"
}
}
// Implementation
async function getUsers(page = 1, perPage = 20) {
const offset = (page - 1) * perPage;
const [users, total] = await Promise.all([
db.users.limit(perPage).offset(offset),
db.users.count()
]);
return {
data: users,
meta: {
page,
per_page: perPage,
total,
total_pages: Math.ceil(total / perPage)
}
};
}Simple to implement and understand
✗Poor performance on large datasets (deep offsets)
2. Cursor-Based Pagination (Efficient)
// Request (first page)
GET /users?limit=20
// Response
{
"data": [...], // 20 users
"pagination": {
"next_cursor": "eyJpZCI6MjB9", // Base64 encoded
"has_more": true
}
}
// Request (next page)
GET /users?limit=20&cursor=eyJpZCI6MjB9
// Implementation
async function getUsers(cursor = null, limit = 20) {
let query = db.users.orderBy('id');
if (cursor) {
const decoded = JSON.parse(atob(cursor));
query = query.where('id', '>', decoded.id);
}
const users = await query.limit(limit + 1);
const hasMore = users.length > limit;
if (hasMore) {
users.pop(); // Remove extra item
}
const nextCursor = hasMore
? btoa(JSON.stringify({ id: users[users.length - 1].id }))
: null;
return {
data: users,
pagination: {
next_cursor: nextCursor,
has_more: hasMore
}
};
}Excellent performance on large datasets
Handles real-time data changes well
⚠Cannot jump to arbitrary pages
3. Keyset Pagination (Time-Based)
// Request
GET /posts?since=2024-01-15T10:30:00Z&limit=20
// Response
{
"data": [...], // 20 posts created after timestamp
"pagination": {
"oldest": "2024-01-15T08:15:00Z",
"newest": "2024-01-15T10:25:00Z"
}
}
// Implementation
async function getPosts(since = null, limit = 20) {
let query = db.posts.orderBy('created_at', 'desc');
if (since) {
query = query.where('created_at', '<', since);
}
const posts = await query.limit(limit);
return {
data: posts,
pagination: {
oldest: posts[posts.length - 1]?.created_at,
newest: posts[0]?.created_at
}
};
}Perfect for feeds and timelines
No duplicates with real-time updates
Rate Limiting
Rate Limit Implementation
Token Bucket Algorithm
// Rate limiter using Redis
class RateLimiter {
constructor(redis) {
this.redis = redis;
}
async checkLimit(userId, limit = 100, window = 900) {
// limit: max requests, window: time in seconds (900s = 15min)
const key = `rate_limit:${userId}`;
const now = Date.now();
// Remove old requests outside window
await this.redis.zremrangebyscore(key, 0, now - (window * 1000));
// Count requests in current window
const requestCount = await this.redis.zcard(key);
if (requestCount >= limit) {
// Rate limit exceeded
const oldest = await this.redis.zrange(key, 0, 0);
const resetTime = parseInt(oldest[0]) + (window * 1000);
return {
allowed: false,
limit,
remaining: 0,
resetAt: new Date(resetTime)
};
}
// Add current request
await this.redis.zadd(key, now, now);
await this.redis.expire(key, window);
return {
allowed: true,
limit,
remaining: limit - requestCount - 1,
resetAt: new Date(now + (window * 1000))
};
}
}
// Middleware usage
app.use(async (req, res, next) => {
const userId = req.user?.id || req.ip;
const result = await rateLimiter.checkLimit(userId);
// Set rate limit headers
res.set({
'X-RateLimit-Limit': result.limit,
'X-RateLimit-Remaining': result.remaining,
'X-RateLimit-Reset': result.resetAt.toISOString()
});
if (!result.allowed) {
return res.status(429).json({
error: 'Too many requests',
retry_after: Math.ceil((result.resetAt - Date.now()) / 1000)
});
}
next();
});Tiered Rate Limits
// Different limits based on user tier
const RATE_LIMITS = {
free: { limit: 100, window: 900 }, // 100 req/15min
basic: { limit: 1000, window: 900 }, // 1000 req/15min
premium: { limit: 10000, window: 900 }, // 10000 req/15min
enterprise: { limit: -1, window: 0 } // Unlimited
};
async function getRateLimit(userId) {
const user = await db.users.findById(userId);
const tier = user.subscription_tier || 'free';
return RATE_LIMITS[tier];
}Client-Side Rate Limit Handling
// Respect rate limits in API client
async function fetchWithRateLimit(url, options = {}) {
const response = await fetch(url, options);
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
console.log(`Rate limit: ${remaining}/${limit} remaining`);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
console.warn(`Rate limited. Retry after ${retryAfter} seconds`);
// Wait and retry
await new Promise(resolve =>
setTimeout(resolve, retryAfter * 1000)
);
return fetchWithRateLimit(url, options);
}
// Warn when approaching limit
if (remaining < limit * 0.1) {
console.warn(`Approaching rate limit: ${remaining} remaining`);
}
return response;
}Query Optimization
Field Selection (Sparse Fieldsets)
Only return requested fields to reduce payload size:
// Request specific fields
GET /users?fields=id,name,email
// Response (only requested fields)
{
"data": [
{ "id": 1, "name": "John", "email": "john@example.com" },
{ "id": 2, "name": "Jane", "email": "jane@example.com" }
]
}
// Implementation
async function getUsers(fields = null) {
let query = db.users;
if (fields) {
const fieldArray = fields.split(',').map(f => f.trim());
query = query.select(fieldArray);
}
return await query;
}
// Savings example:
// Full user object: ~2KB
// Selected fields: ~200 bytes
// 90% reduction in payload size!Eager Loading (N+1 Problem)
// ❌ BAD: N+1 queries
async function getUsersWithPosts() {
const users = await db.users.findAll(); // 1 query
for (const user of users) {
user.posts = await db.posts.where({ user_id: user.id }); // N queries
}
return users;
}
// Total: 1 + N queries (if 100 users = 101 queries!)
// ✅ GOOD: Single query with join
async function getUsersWithPosts() {
return await db.users
.include('posts') // Single JOIN query
.findAll();
}
// Total: 1 query
// Alternative: Batch loading
async function getUsersWithPosts() {
const users = await db.users.findAll();
const userIds = users.map(u => u.id);
const posts = await db.posts
.whereIn('user_id', userIds)
.findAll();
// Group posts by user
const postsByUser = posts.reduce((acc, post) => {
if (!acc[post.user_id]) acc[post.user_id] = [];
acc[post.user_id].push(post);
return acc;
}, {});
users.forEach(user => {
user.posts = postsByUser[user.id] || [];
});
return users;
}
// Total: 2 queries (much better!)Database Indexing
// Create indexes on frequently queried fields 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); // Composite index for multiple filters CREATE INDEX idx_products_category_price ON products(category, price); // Before indexing: 2500ms query time // After indexing: 15ms query time // 166x faster! 🚀
Connection Pooling
// Database connection pool
const pool = new Pool({
host: 'localhost',
database: 'myapp',
max: 20, // Maximum connections
min: 5, // Minimum connections
idle: 10000 // Close idle connections after 10s
});
// Reuse connections instead of creating new ones
async function query(sql, params) {
const client = await pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release(); // Return to pool
}
}Response Compression
GZIP Compression
Compress responses to reduce bandwidth usage by 70-90%:
Express.js Implementation
import compression from 'compression';
import express from 'express';
import { CodeBlock } from '@/app/components/CodeBlock';
const app = express();
// Enable GZIP compression
app.use(compression({
level: 6, // Compression level (0-9)
threshold: 1024, // Only compress responses > 1KB
filter: (req, res) => {
// Don't compress if client doesn't support it
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));Compression Impact
| Content Type | Original | Compressed | Savings |
|---|---|---|---|
| JSON (API response) | 100 KB | 15 KB | 85% |
| HTML | 80 KB | 20 KB | 75% |
| CSS/JavaScript | 200 KB | 50 KB | 75% |
Performance Best Practices
Implement caching: Use HTTP headers, ETags, and server-side caching (Redis)
Always paginate: Use cursor-based pagination for large datasets
Rate limit requests: Protect your API with token bucket or sliding window algorithms
Enable GZIP compression: Reduce bandwidth by 70-90%
Support field selection: Let clients request only needed fields
Optimize database queries: Add indexes, avoid N+1 queries, use connection pooling
Monitor performance: Track response times, cache hit rates, error rates
Use CDN for static content: Serve from edge locations close to users
Async operations: Use non-blocking I/O for better concurrency
Set appropriate timeouts: Prevent resource exhaustion from slow requests