Rate Limiting Strategies for Public APIs: Complete Implementation Guide
Rate limiting is one of the most critical defenses for public APIs. Without proper rate limiting, your API is vulnerable to abuse, denial-of-service attacks, resource exhaustion, and excessive costs.
According to Cloudflare’s 2025 API Security Report, over 30% of API traffic is malicious or abusive. Rate limiting is your first line of defense against this threat landscape.
In this comprehensive guide, we’ll explore production-ready rate limiting strategies, from basic implementations to distributed systems, with real-world code examples in Node.js, Redis, and Nginx.
Why Rate Limiting Matters
Rate limiting protects your API from:
1. Denial of Service (DoS) Attacks
Malicious actors flooding your API with requests to overwhelm your infrastructure.
2. Brute Force Attacks
Automated password guessing, credential stuffing, or API key enumeration attempts.
3. Resource Exhaustion
Unintentional abuse from poorly implemented clients or runaway scripts.
4. Cost Control
Preventing unexpected cloud bills from excessive API usage, especially with third-party service dependencies.
5. Fair Usage
Ensuring all users get reasonable access to shared resources.
6. Data Scraping
Preventing competitors or malicious actors from harvesting your data at scale.
Rate Limiting Algorithms
Different use cases require different algorithms. Let’s explore the most common strategies.
1. Fixed Window Counter
The simplest approach: count requests in fixed time windows.
How it works:
- Divide time into fixed windows (e.g., 1-minute intervals)
- Count requests within each window
- Reset counter at window boundaries
Pros: Simple to implement, low memory usage
Cons: Burst traffic at window boundaries (the “thundering herd” problem)
// Basic fixed window implementation
class FixedWindowRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.windows = new Map(); // key -> { count, resetAt }
}
isAllowed(key) {
const now = Date.now();
const window = this.windows.get(key);
// No window exists or window expired
if (!window || now >= window.resetAt) {
this.windows.set(key, {
count: 1,
resetAt: now + this.windowMs
});
return { allowed: true, remaining: this.maxRequests - 1 };
}
// Within current window
if (window.count < this.maxRequests) {
window.count++;
return {
allowed: true,
remaining: this.maxRequests - window.count
};
}
// Rate limit exceeded
return {
allowed: false,
remaining: 0,
resetAt: window.resetAt
};
}
}
// Usage
const limiter = new FixedWindowRateLimiter(100, 60000); // 100 requests per minute
const result = limiter.isAllowed('user:123');
if (!result.allowed) {
console.log(`Rate limit exceeded. Try again at ${new Date(result.resetAt)}`);
}
Problem: At 00:59, a user makes 100 requests. At 01:01, they make another 100 requests. That’s 200 requests in 2 seconds, bypassing the intended limit.
2. Sliding Window Log
Track exact timestamps of each request.
How it works:
- Store timestamps of all requests
- Remove timestamps older than the window
- Count remaining timestamps
Pros: Prevents burst traffic at boundaries, precise
Cons: Higher memory usage (stores all timestamps)
class SlidingWindowLogRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.logs = new Map(); // key -> [timestamps]
}
isAllowed(key) {
const now = Date.now();
const timestamps = this.logs.get(key) || [];
// Remove timestamps outside the current window
const validTimestamps = timestamps.filter(
timestamp => now - timestamp < this.windowMs
);
if (validTimestamps.length < this.maxRequests) {
validTimestamps.push(now);
this.logs.set(key, validTimestamps);
return {
allowed: true,
remaining: this.maxRequests - validTimestamps.length
};
}
// Rate limit exceeded
const oldestTimestamp = validTimestamps[0];
return {
allowed: false,
remaining: 0,
resetAt: oldestTimestamp + this.windowMs
};
}
}
// Usage
const limiter = new SlidingWindowLogRateLimiter(100, 60000);
3. Sliding Window Counter (Hybrid)
Combines fixed window efficiency with sliding window accuracy.
How it works:
- Maintain counts for current and previous windows
- Calculate weighted average based on time position in current window
Pros: Good balance of accuracy and efficiency
Cons: Approximation, not exact
class SlidingWindowCounterRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.counters = new Map(); // key -> { current, previous, resetAt }
}
isAllowed(key) {
const now = Date.now();
let counter = this.counters.get(key);
if (!counter || now >= counter.resetAt) {
// Start new window
this.counters.set(key, {
current: 1,
previous: counter?.current || 0,
resetAt: now + this.windowMs
});
return { allowed: true, remaining: this.maxRequests - 1 };
}
// Calculate weighted count from current and previous windows
const timeIntoWindow = now - (counter.resetAt - this.windowMs);
const percentageIntoWindow = timeIntoWindow / this.windowMs;
const weightedCount =
counter.current +
counter.previous * (1 - percentageIntoWindow);
if (weightedCount < this.maxRequests) {
counter.current++;
return {
allowed: true,
remaining: Math.floor(this.maxRequests - weightedCount - 1)
};
}
return {
allowed: false,
remaining: 0,
resetAt: counter.resetAt
};
}
}
4. Token Bucket
The most flexible and widely used algorithm.
How it works:
- Bucket holds tokens (up to a maximum capacity)
- Tokens are added at a fixed rate
- Each request consumes a token
- Request denied if bucket is empty
Pros: Handles bursts gracefully, highly configurable
Cons: More complex implementation
class TokenBucketRateLimiter {
constructor(capacity, refillRate, refillIntervalMs) {
this.capacity = capacity; // Maximum tokens
this.refillRate = refillRate; // Tokens added per interval
this.refillIntervalMs = refillIntervalMs;
this.buckets = new Map(); // key -> { tokens, lastRefill }
}
isAllowed(key, cost = 1) {
const now = Date.now();
let bucket = this.buckets.get(key);
if (!bucket) {
bucket = {
tokens: this.capacity - cost,
lastRefill: now
};
this.buckets.set(key, bucket);
return { allowed: true, remaining: bucket.tokens };
}
// Refill tokens based on elapsed time
const elapsedMs = now - bucket.lastRefill;
const intervalsElapsed = Math.floor(elapsedMs / this.refillIntervalMs);
if (intervalsElapsed > 0) {
bucket.tokens = Math.min(
this.capacity,
bucket.tokens + (intervalsElapsed * this.refillRate)
);
bucket.lastRefill = now;
}
// Check if enough tokens available
if (bucket.tokens >= cost) {
bucket.tokens -= cost;
return {
allowed: true,
remaining: bucket.tokens
};
}
return {
allowed: false,
remaining: bucket.tokens
};
}
}
// Usage: 100 tokens capacity, refill 10 tokens every second
const limiter = new TokenBucketRateLimiter(100, 10, 1000);
// Expensive operation costs more tokens
const result = limiter.isAllowed('user:123', 5);
5. Leaky Bucket
Process requests at a constant rate, queueing excess.
How it works:
- Requests enter a queue (bucket)
- Requests are processed at a fixed rate
- Queue has maximum capacity
Pros: Smooth traffic, predictable processing rate
Cons: Can introduce latency
class LeakyBucketRateLimiter {
constructor(capacity, leakRate) {
this.capacity = capacity; // Maximum queue size
this.leakRate = leakRate; // Requests per second
this.queues = new Map(); // key -> { queue, lastLeak }
}
async process(key, requestHandler) {
const now = Date.now();
let bucket = this.queues.get(key);
if (!bucket) {
bucket = { queue: [], lastLeak: now };
this.queues.set(key, bucket);
}
// Leak (process) requests at fixed rate
const elapsedMs = now - bucket.lastLeak;
const requestsToLeak = Math.floor((elapsedMs / 1000) * this.leakRate);
if (requestsToLeak > 0) {
const leaked = bucket.queue.splice(0, requestsToLeak);
bucket.lastLeak = now;
// Process leaked requests
for (const handler of leaked) {
await handler();
}
}
// Add new request to queue
if (bucket.queue.length < this.capacity) {
return new Promise((resolve, reject) => {
bucket.queue.push(async () => {
try {
const result = await requestHandler();
resolve(result);
} catch (error) {
reject(error);
}
});
});
}
throw new Error('Rate limit exceeded: queue full');
}
}
Production Implementation with Express and Redis
For production APIs, use Redis for distributed rate limiting across multiple server instances.
Express Middleware with Redis
// rate-limiter.js
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
});
class RedisRateLimiter {
constructor(options = {}) {
this.maxRequests = options.maxRequests || 100;
this.windowMs = options.windowMs || 60000;
this.keyPrefix = options.keyPrefix || 'rl:';
}
async slidingWindowCounter(key) {
const now = Date.now();
const windowStart = now - this.windowMs;
const redisKey = `${this.keyPrefix}${key}`;
const pipeline = redis.pipeline();
// Remove old entries
pipeline.zremrangebyscore(redisKey, 0, windowStart);
// Count requests in current window
pipeline.zcard(redisKey);
// Add current request
pipeline.zadd(redisKey, now, `${now}-${Math.random()}`);
// Set expiration
pipeline.expire(redisKey, Math.ceil(this.windowMs / 1000));
const results = await pipeline.exec();
const count = results[1][1]; // Get count from zcard
const allowed = count < this.maxRequests;
const remaining = Math.max(0, this.maxRequests - count - 1);
return {
allowed,
remaining,
limit: this.maxRequests,
resetAt: now + this.windowMs
};
}
async tokenBucket(key, cost = 1) {
const now = Date.now();
const redisKey = `${this.keyPrefix}tb:${key}`;
// Lua script for atomic token bucket operation
const script = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local refillInterval = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local now = tonumber(ARGV[5])
local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or capacity
local lastRefill = tonumber(bucket[2]) or now
-- Refill tokens
local elapsed = now - lastRefill
local intervals = math.floor(elapsed / refillInterval)
if intervals > 0 then
tokens = math.min(capacity, tokens + (intervals * refillRate))
lastRefill = now
end
-- Check if request allowed
if tokens >= cost then
tokens = tokens - cost
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', lastRefill)
redis.call('EXPIRE', key, 3600)
return {1, tokens}
else
return {0, tokens}
end
`;
const capacity = this.maxRequests;
const refillRate = Math.ceil(this.maxRequests / (this.windowMs / 1000));
const refillInterval = 1000; // 1 second
const result = await redis.eval(
script,
1,
redisKey,
capacity,
refillRate,
refillInterval,
cost,
now
);
const allowed = result[0] === 1;
const remaining = result[1];
return {
allowed,
remaining,
limit: capacity
};
}
middleware(options = {}) {
const algorithm = options.algorithm || 'sliding-window';
const keyGenerator = options.keyGenerator || ((req) => req.ip);
const handler = options.handler || this.defaultHandler;
return async (req, res, next) => {
try {
const key = keyGenerator(req);
let result;
if (algorithm === 'token-bucket') {
result = await this.tokenBucket(key, options.cost || 1);
} else {
result = await this.slidingWindowCounter(key);
}
// Set standard rate limit headers
res.setHeader('X-RateLimit-Limit', result.limit);
res.setHeader('X-RateLimit-Remaining', result.remaining);
if (result.resetAt) {
res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetAt / 1000));
}
if (!result.allowed) {
return handler(req, res);
}
next();
} catch (error) {
console.error('Rate limiting error:', error);
// Fail open - allow request if rate limiter fails
next();
}
};
}
defaultHandler(req, res) {
res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
retryAfter: res.getHeader('X-RateLimit-Reset')
});
}
}
module.exports = RedisRateLimiter;
Using the Middleware
// server.js
const express = require('express');
const RedisRateLimiter = require('./rate-limiter');
const app = express();
// Global rate limiter: 100 requests per minute
const globalLimiter = new RedisRateLimiter({
maxRequests: 100,
windowMs: 60000,
keyPrefix: 'global:'
});
app.use(globalLimiter.middleware({
keyGenerator: (req) => req.ip
}));
// API key-based rate limiter: 1000 requests per hour
const apiKeyLimiter = new RedisRateLimiter({
maxRequests: 1000,
windowMs: 3600000,
keyPrefix: 'api:'
});
app.use('/api', apiKeyLimiter.middleware({
keyGenerator: (req) => req.headers['x-api-key'] || req.ip
}));
// Expensive endpoint with token bucket
const expensiveLimiter = new RedisRateLimiter({
maxRequests: 10,
windowMs: 60000,
keyPrefix: 'expensive:'
});
app.post('/api/expensive-operation', expensiveLimiter.middleware({
algorithm: 'token-bucket',
cost: 5, // Costs 5 tokens
keyGenerator: (req) => req.user?.id || req.ip
}), (req, res) => {
// Expensive operation
res.json({ success: true });
});
// Different limits for authenticated vs anonymous users
app.get('/api/data', (req, res, next) => {
const limiter = req.user
? new RedisRateLimiter({ maxRequests: 1000, windowMs: 60000 })
: new RedisRateLimiter({ maxRequests: 100, windowMs: 60000 });
return limiter.middleware({
keyGenerator: (req) => req.user?.id || req.ip
})(req, res, next);
}, (req, res) => {
res.json({ data: [...] });
});
app.listen(3000, () => {
console.log('API server running on port 3000');
});
Advanced Strategies
1. Tiered Rate Limiting
Different limits based on user tier:
const RATE_LIMIT_TIERS = {
free: { maxRequests: 100, windowMs: 3600000 }, // 100/hour
basic: { maxRequests: 1000, windowMs: 3600000 }, // 1000/hour
premium: { maxRequests: 10000, windowMs: 3600000 }, // 10000/hour
enterprise: { maxRequests: 100000, windowMs: 3600000 } // 100000/hour
};
function getTierLimiter(userTier) {
const config = RATE_LIMIT_TIERS[userTier] || RATE_LIMIT_TIERS.free;
return new RedisRateLimiter(config);
}
app.use('/api', async (req, res, next) => {
const tier = req.user?.tier || 'free';
const limiter = getTierLimiter(tier);
return limiter.middleware({
keyGenerator: (req) => `${tier}:${req.user?.id || req.ip}`
})(req, res, next);
});
2. Dynamic Rate Limiting
Adjust limits based on server load:
const os = require('os');
function getDynamicLimit() {
const cpuUsage = os.loadavg()[0] / os.cpus().length;
if (cpuUsage > 0.8) {
return { maxRequests: 50, windowMs: 60000 }; // Reduce under load
} else if (cpuUsage > 0.5) {
return { maxRequests: 100, windowMs: 60000 };
} else {
return { maxRequests: 200, windowMs: 60000 }; // Normal load
}
}
app.use((req, res, next) => {
const config = getDynamicLimit();
const limiter = new RedisRateLimiter(config);
return limiter.middleware()(req, res, next);
});
3. Geographic Rate Limiting
Different limits based on geographic location:
const geoip = require('geoip-lite');
const GEO_RATE_LIMITS = {
US: { maxRequests: 200, windowMs: 60000 },
EU: { maxRequests: 200, windowMs: 60000 },
default: { maxRequests: 100, windowMs: 60000 }
};
app.use((req, res, next) => {
const geo = geoip.lookup(req.ip);
const region = geo?.country || 'default';
const config = GEO_RATE_LIMITS[region] || GEO_RATE_LIMITS.default;
const limiter = new RedisRateLimiter(config);
return limiter.middleware({
keyGenerator: (req) => `${region}:${req.ip}`
})(req, res, next);
});
4. Endpoint-Specific Limits
const ENDPOINT_LIMITS = {
'GET:/api/users': { maxRequests: 1000, windowMs: 60000 },
'POST:/api/users': { maxRequests: 10, windowMs: 60000 },
'GET:/api/search': { maxRequests: 100, windowMs: 60000 },
'POST:/api/auth/login': { maxRequests: 5, windowMs: 300000 }, // 5 per 5 min
};
app.use((req, res, next) => {
const endpoint = `${req.method}:${req.path}`;
const config = ENDPOINT_LIMITS[endpoint] || { maxRequests: 100, windowMs: 60000 };
const limiter = new RedisRateLimiter(config);
return limiter.middleware({
keyGenerator: (req) => `${endpoint}:${req.user?.id || req.ip}`
})(req, res, next);
});
Rate Limiting with Nginx
For high-performance rate limiting at the edge, use Nginx:
# nginx.conf
http {
# Define rate limit zones
# Global IP-based limit: 100 requests per minute
limit_req_zone $binary_remote_addr zone=global:10m rate=100r/m;
# API key-based limit: 1000 requests per minute
limit_req_zone $http_x_api_key zone=api_key:10m rate=1000r/m;
# Login endpoint: 5 requests per minute
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# Expensive operations: 10 requests per minute
limit_req_zone $binary_remote_addr zone=expensive:10m rate=10r/m;
server {
listen 80;
server_name api.example.com;
# Global rate limit
location /api {
limit_req zone=global burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
# Strict rate limit for login
location /api/auth/login {
limit_req zone=login burst=2;
limit_req_status 429;
proxy_pass http://backend;
}
# Expensive operations
location /api/reports {
limit_req zone=expensive burst=5;
limit_req_status 429;
proxy_pass http://backend;
}
# API key-based rate limiting
location /api/premium {
# Use API key if present, otherwise IP
set $limit_key $http_x_api_key;
if ($limit_key = "") {
set $limit_key $binary_remote_addr;
}
limit_req zone=api_key burst=50 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
# Custom error page for rate limits
error_page 429 /rate_limit_error.json;
location = /rate_limit_error.json {
internal;
default_type application/json;
return 429 '{"error":"Too Many Requests","message":"Rate limit exceeded"}';
}
}
}
Cloudflare Rate Limiting
For global CDN-level rate limiting:
// Cloudflare Worker for advanced rate limiting
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const clientIP = request.headers.get('CF-Connecting-IP');
const apiKey = request.headers.get('X-API-Key');
// Use API key if present, otherwise IP
const identifier = apiKey || clientIP;
// Define rate limit
const rateLimit = {
limit: 100,
period: 60 // seconds
};
// Rate limit key
const key = `rate_limit:${identifier}:${url.pathname}`;
// Check rate limit using Cloudflare KV
const current = await RATE_LIMIT_KV.get(key);
const count = current ? parseInt(current) : 0;
if (count >= rateLimit.limit) {
return new Response(JSON.stringify({
error: 'Rate limit exceeded',
limit: rateLimit.limit,
period: rateLimit.period
}), {
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': rateLimit.limit,
'X-RateLimit-Remaining': 0
}
});
}
// Increment counter
await RATE_LIMIT_KV.put(key, (count + 1).toString(), {
expirationTtl: rateLimit.period
});
// Forward request
const response = await fetch(request);
// Add rate limit headers
const newHeaders = new Headers(response.headers);
newHeaders.set('X-RateLimit-Limit', rateLimit.limit);
newHeaders.set('X-RateLimit-Remaining', rateLimit.limit - count - 1);
return new Response(response.body, {
status: response.status,
headers: newHeaders
});
}
Best Practices
✅ Return Standard Headers
Follow RFC 6585 and provide clear rate limit information:
res.setHeader('X-RateLimit-Limit', 100);
res.setHeader('X-RateLimit-Remaining', 95);
res.setHeader('X-RateLimit-Reset', 1659312000);
res.setHeader('Retry-After', 60);
✅ Use Multiple Layers
Combine different rate limiting strategies:
// Layer 1: Nginx (edge, fast)
// Layer 2: Application middleware (business logic)
// Layer 3: Per-endpoint limits
// Layer 4: Per-user tier limits
✅ Fail Open on Errors
If your rate limiter fails, allow the request through:
try {
const result = await rateLimiter.check(key);
if (!result.allowed) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
} catch (error) {
console.error('Rate limiter error:', error);
// Fail open - allow request
}
✅ Monitor and Alert
Track rate limit metrics:
const { Counter, Histogram } = require('prom-client');
const rateLimitCounter = new Counter({
name: 'rate_limit_total',
help: 'Total rate limit checks',
labelNames: ['result', 'endpoint']
});
const rateLimitExceeded = new Counter({
name: 'rate_limit_exceeded_total',
help: 'Total rate limit violations',
labelNames: ['identifier', 'endpoint']
});
// In middleware
if (result.allowed) {
rateLimitCounter.inc({ result: 'allowed', endpoint: req.path });
} else {
rateLimitCounter.inc({ result: 'blocked', endpoint: req.path });
rateLimitExceeded.inc({ identifier: key, endpoint: req.path });
}
✅ Provide Clear Error Messages
res.status(429).json({
error: 'Rate Limit Exceeded',
message: 'You have exceeded the rate limit for this endpoint',
limit: 100,
remaining: 0,
resetAt: new Date(resetTimestamp).toISOString(),
retryAfter: 60,
documentation: 'https://api.example.com/docs/rate-limits'
});
✅ Consider Cost-Based Limiting
Different operations consume different resources:
const OPERATION_COSTS = {
'GET:/api/users/:id': 1,
'GET:/api/search': 5,
'POST:/api/reports': 10,
'POST:/api/bulk-import': 50
};
const cost = OPERATION_COSTS[endpoint] || 1;
const result = await tokenBucketLimiter.check(key, cost);
Testing Rate Limiters
// rate-limiter.test.js
const { expect } = require('chai');
const RedisRateLimiter = require('./rate-limiter');
describe('Rate Limiter', () => {
let limiter;
beforeEach(() => {
limiter = new RedisRateLimiter({
maxRequests: 5,
windowMs: 1000
});
});
it('should allow requests under limit', async () => {
for (let i = 0; i < 5; i++) {
const result = await limiter.slidingWindowCounter('test-key');
expect(result.allowed).to.be.true;
}
});
it('should block requests over limit', async () => {
// Exhaust limit
for (let i = 0; i < 5; i++) {
await limiter.slidingWindowCounter('test-key');
}
// Next request should be blocked
const result = await limiter.slidingWindowCounter('test-key');
expect(result.allowed).to.be.false;
});
it('should reset after window expires', async () => {
// Exhaust limit
for (let i = 0; i < 5; i++) {
await limiter.slidingWindowCounter('test-key');
}
// Wait for window to expire
await new Promise(resolve => setTimeout(resolve, 1100));
// Should allow requests again
const result = await limiter.slidingWindowCounter('test-key');
expect(result.allowed).to.be.true;
});
it('should track different keys independently', async () => {
await limiter.slidingWindowCounter('key1');
await limiter.slidingWindowCounter('key2');
const result1 = await limiter.slidingWindowCounter('key1');
const result2 = await limiter.slidingWindowCounter('key2');
expect(result1.remaining).to.equal(3);
expect(result2.remaining).to.equal(3);
});
});
Conclusion
Rate limiting is essential for building robust, secure, and scalable APIs. By implementing the strategies outlined in this guide, you can:
- Protect your API from abuse and DoS attacks
- Ensure fair resource allocation across users
- Control costs and prevent resource exhaustion
- Provide a better experience for legitimate users
- Meet SLA commitments and maintain service quality
Key Takeaways:
- Choose the right algorithm for your use case (token bucket for flexibility, sliding window for accuracy)
- Use Redis for distributed rate limiting across multiple servers
- Implement multiple layers (edge, application, endpoint, user tier)
- Return standard rate limit headers
- Monitor violations and adjust limits based on real usage patterns
- Fail open on errors to maintain availability
Start with simple IP-based rate limiting, then evolve to more sophisticated strategies as your API grows.
For more API security best practices, check out our guides on API Design Patterns and JWT Authentication.
Ready to implement rate limiting? Start with the Redis-based sliding window implementation for production-ready, distributed rate limiting that scales with your infrastructure.