Skip to main content

Rate Limiting

Wit’s server includes a flexible rate limiting middleware to protect API endpoints from abuse. The rate limiter supports multiple storage backends and provides fine-grained control over request limits.

Overview

Rate limiting features:
  • Configurable limits - Set requests per time window
  • Multiple stores - Memory, Redis, or sliding window
  • Preset configurations - Ready-to-use limits for common endpoints
  • User-aware limits - Different limits for authenticated users
  • Trusted bypass - Skip limits for trusted sources

Basic Usage

import { rateLimit, RateLimitPresets } from 'wit/server/middleware/rate-limit';
import { Hono } from 'hono';

const app = new Hono();

// Apply to all routes
app.use('*', rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
}));

// Or use presets
app.use('/api/*', rateLimit(RateLimitPresets.standard));

Configuration Options

OptionTypeDefaultDescription
windowMsnumberrequiredTime window in milliseconds
maxnumberrequiredMax requests per window
keyGeneratorfunctionIP-basedGenerate rate limit key
handlerfunctionJSON 429Custom rate limit response
skipfunctionnoneSkip rate limiting for request
keyPrefixstring’ratelimit’Prefix for storage keys
headersbooleantrueSend rate limit headers
messagestring’Too many requests’Error message

Presets

Wit includes presets for common use cases:

Standard

100 requests per minute for general API endpoints:
app.use('/api/*', rateLimit(RateLimitPresets.standard));

Strict

5 requests per minute for sensitive endpoints:
app.use('/api/admin/*', rateLimit(RateLimitPresets.strict));

Auth

3 requests per 15 minutes for authentication:
app.use('/api/auth/login', rateLimit(RateLimitPresets.auth));
20 requests per minute for expensive operations:
app.use('/api/search', rateLimit(RateLimitPresets.search));

Upload

30 requests per minute for file uploads:
app.use('/api/upload', rateLimit(RateLimitPresets.upload));

Webhook

60 requests per minute for webhook endpoints:
app.use('/webhooks/*', rateLimit(RateLimitPresets.webhook));

Relaxed

300 requests per minute for read-only endpoints:
app.use('/api/public/*', rateLimit(RateLimitPresets.relaxed));

Preset Details

PresetRequestsWindowUse Case
strict51 minAdmin, sensitive ops
standard1001 minGeneral API
relaxed3001 minRead-only, public
auth315 minLogin, registration
upload301 minFile uploads
webhook601 minExternal webhooks
search201 minSearch, AI features

Response Headers

When headers: true (default), rate limit info is included in responses:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1703520000
When rate limited:
Retry-After: 45

Custom Key Generation

By default, rate limiting is based on IP address. Customize this:
rateLimit({
  windowMs: 60000,
  max: 100,
  keyGenerator: (c) => {
    // Rate limit by user ID if authenticated
    const userId = c.get('userId');
    if (userId) return `user:${userId}`;
    
    // Fall back to IP
    return c.req.header('x-forwarded-for') || 'unknown';
  },
});

User-Aware Rate Limiting

Different limits for authenticated vs anonymous users:
import { userRateLimit } from 'wit/server/middleware/rate-limit';

app.use('/api/*', userRateLimit({
  windowMs: 60000,
  max: 100, // Default
  authenticatedMax: 500, // Higher for logged-in users
  unauthenticatedMax: 50, // Lower for anonymous
  getUserId: async (c) => {
    const session = c.get('session');
    return session?.userId || null;
  },
}));

Trusted Source Bypass

Skip rate limiting for trusted sources:
import { rateLimit, createTrustedBypass } from 'wit/server/middleware/rate-limit';

const bypass = createTrustedBypass({
  trustedIPs: ['192.168.1.0/24', '10.0.0.0/8'],
  trustedApiKeys: ['sk-trusted-key-1', 'sk-trusted-key-2'],
  apiKeyHeader: 'x-api-key',
  isTrusted: async (c) => {
    // Custom trust logic
    return c.req.header('x-internal') === 'true';
  },
});

app.use('/api/*', rateLimit({
  windowMs: 60000,
  max: 100,
  skip: bypass,
}));

Storage Backends

Memory Store (Default)

Suitable for single-instance deployments:
import { MemoryStore, setRateLimitStore } from 'wit/server/middleware/rate-limit';

const store = new MemoryStore(60000); // cleanup interval
setRateLimitStore(store);

Redis Store

For distributed/multi-instance deployments:
import { RedisStore, setRateLimitStore } from 'wit/server/middleware/rate-limit';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const store = new RedisStore(redis);
setRateLimitStore(store);

Sliding Window Store

More accurate limiting using Redis sorted sets:
import { SlidingWindowStore, setRateLimitStore } from 'wit/server/middleware/rate-limit';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const store = new SlidingWindowStore(redis);
setRateLimitStore(store);

Endpoint-Specific Limits

Use the factory function for type-safe endpoint limits:
import { rateLimitForEndpoint } from 'wit/server/middleware/rate-limit';

// Type-safe endpoint types
app.get('/api/repos', rateLimitForEndpoint('read'));
app.post('/api/repos', rateLimitForEndpoint('write'));
app.post('/api/auth/login', rateLimitForEndpoint('auth'));
app.get('/api/search', rateLimitForEndpoint('search'));
app.post('/api/upload', rateLimitForEndpoint('upload'));
app.post('/webhooks/github', rateLimitForEndpoint('webhook'));
app.get('/public/*', rateLimitForEndpoint('public'));

// With overrides
app.get('/api/heavy', rateLimitForEndpoint('search', {
  max: 5, // Override default
}));

Custom Error Handler

rateLimit({
  windowMs: 60000,
  max: 100,
  handler: (c) => {
    return c.json({
      error: 'Rate limit exceeded',
      code: 'RATE_LIMIT_EXCEEDED',
      retryAfter: c.res.headers.get('Retry-After'),
    }, 429);
  },
});

Accessing Rate Limit Info

Access rate limit info in route handlers:
app.get('/api/status', (c) => {
  const rateLimit = c.get('rateLimit');
  
  return c.json({
    rateLimit: {
      limit: rateLimit.limit,
      remaining: rateLimit.remaining,
      resetIn: rateLimit.resetTime,
    },
  });
});

Best Practices

Use Redis storage for production deployments with multiple instances.
Set stricter limits on authentication endpoints to prevent brute force attacks.
Use the sliding window algorithm for more accurate rate limiting at the cost of slightly higher Redis load.
Memory store data is lost on restart. Use Redis for persistence.
Rate limits based on IP can be bypassed with proxies. Consider user-based limits for authenticated endpoints.

Example: Complete Setup

import { Hono } from 'hono';
import {
  rateLimit,
  userRateLimit,
  RateLimitPresets,
  RedisStore,
  setRateLimitStore,
  createTrustedBypass,
} from 'wit/server/middleware/rate-limit';
import Redis from 'ioredis';

const app = new Hono();

// Setup Redis store
const redis = new Redis(process.env.REDIS_URL);
setRateLimitStore(new RedisStore(redis));

// Trusted bypass
const trustedBypass = createTrustedBypass({
  trustedIPs: ['10.0.0.0/8'],
  trustedApiKeys: [process.env.INTERNAL_API_KEY],
});

// Global rate limit
app.use('*', rateLimit({
  ...RateLimitPresets.relaxed,
  skip: trustedBypass,
}));

// Auth endpoints - strict
app.use('/api/auth/*', rateLimit(RateLimitPresets.auth));

// API endpoints - user-aware
app.use('/api/*', userRateLimit({
  ...RateLimitPresets.standard,
  authenticatedMax: 1000,
  unauthenticatedMax: 100,
  getUserId: (c) => c.get('session')?.userId,
}));

// Search - expensive
app.use('/api/search', rateLimit(RateLimitPresets.search));

export default app;