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 Modified
Server-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 TypeOriginalCompressedSavings
JSON (API response)100 KB15 KB85%
HTML80 KB20 KB75%
CSS/JavaScript200 KB50 KB75%
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