Rate Limiting and Caching
Protecting Your API from Abuse
Every public API is a target. Without limits, a single bad actor (or a runaway script) can flood your server with thousands of requests per second, degrading the service for every other user. Rate limiting caps how many requests a client can make in a given time window.
Caching complements rate limiting by reducing how much work your server does for repeated requests, improving response speed and reducing database load.
What Rate Limiting Does
Rate limiting tracks the number of requests from a source (typically an IP address or authenticated user) within a sliding time window. When the limit is exceeded, the server responds with a 429 Too Many Requests status.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{ "message": "Too many requests, please try again later." }
Setting Up express-rate-limit
npm install express-rate-limit
Global Limit
Apply a loose limit to all routes as a baseline:
const rateLimit = require('express-rate-limit');
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per IP per window
standardHeaders: true, // send RateLimit-* headers
legacyHeaders: false,
message: {
success: false,
message: 'Too many requests from this IP. Please try again in 15 minutes.',
},
});
app.use('/api', globalLimiter);
Stricter Limit for Sensitive Routes
Authentication endpoints are prime targets for brute-force attacks. Apply a tighter limit:
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour window
max: 10, // max 10 login attempts per hour
message: {
success: false,
message: 'Too many login attempts. Please try again in one hour.',
},
});
app.use('/api/v1/auth/login', authLimiter);
app.use('/api/v1/auth/register', authLimiter);
HTTP Caching Headers
HTTP has built-in caching mechanisms. By setting the right response headers, you instruct browsers and CDN (Content Delivery Network) proxies to cache your responses and avoid hitting your server unnecessarily.
Cache-Control
// Cache a public resource for 1 hour
res.set('Cache-Control', 'public, max-age=3600');
// Never cache sensitive or user-specific data
res.set('Cache-Control', 'no-store');
// Cache but revalidate with server before using
res.set('Cache-Control', 'no-cache');
| Directive | Meaning |
|---|---|
public | Can be cached by any cache (browser, CDN) |
private | Only the user's browser can cache it |
max-age=N | Cache is valid for N seconds |
no-store | Never cache this response |
no-cache | Cache but revalidate before using |
ETag (Entity Tag)
An ETag is a fingerprint of a response. Clients send it back on the next request; if the content has not changed, the server responds with 304 Not Modified and no body — saving bandwidth.
const crypto = require('crypto');
app.get('/api/v1/products/:id', async (req, res) => {
const product = await Product.findById(req.params.id);
const etag = crypto.createHash('md5').update(JSON.stringify(product)).digest('hex');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.set('ETag', etag);
res.set('Cache-Control', 'public, max-age=60');
res.json({ success: true, data: product });
});
In-Memory Caching with node-cache
HTTP caching works at the client/CDN level. Server-side caching stores results in memory so you avoid hitting the database for repeated identical queries.
npm install node-cache
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5-minute default TTL
app.get('/api/v1/products', async (req, res) => {
const cacheKey = 'all-products';
const cached = cache.get(cacheKey);
if (cached) {
return res.status(200).json({ success: true, data: cached, source: 'cache' });
}
const products = await Product.find();
cache.set(cacheKey, products);
res.status(200).json({ success: true, data: products, source: 'database' });
});
When data changes (create, update, delete), invalidate the relevant cache key:
cache.del('all-products');
Redis: Remote Dictionary Server
Redis (Remote Dictionary Server) is an in-memory data store used as a cache in production systems. Unlike node-cache, Redis:
- Persists across server restarts
- Is shared across multiple server instances (essential for horizontal scaling)
- Supports advanced data structures (lists, sets, sorted sets)
- Has built-in TTL (Time to Live) management
npm install ioredis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
app.get('/api/v1/products', async (req, res) => {
const cached = await redis.get('all-products');
if (cached) {
return res.json({ success: true, data: JSON.parse(cached), source: 'redis' });
}
const products = await Product.find();
await redis.setex('all-products', 300, JSON.stringify(products)); // expire in 300 seconds
res.json({ success: true, data: products, source: 'database' });
});
For small projects, node-cache is sufficient. For production apps with multiple servers, use Redis.
Practice Exercise
- Add
express-rate-limitto your Express app with a global limit of 100 requests per 15 minutes. - Add a stricter limit (5 requests per hour) to a
/auth/loginroute. - Add
Cache-Control: public, max-age=60headers to a GET endpoint that returns a product list. - Implement server-side caching with
node-cacheon that same endpoint, with a 5-minute TTL (Time to Live). - Log whether each response came from cache or database.
Try it yourself
Key Takeaways
- Rate limiting caps requests per time window per client and returns 429 Too Many Requests when exceeded.
- Apply a loose global limit to all routes and a much stricter limit to sensitive endpoints like login.
- Cache-Control headers instruct browsers and CDNs how long to cache responses, reducing server load.
- ETags allow conditional requests: if content has not changed, the server returns 304 Not Modified with no body.
- Use node-cache for simple single-server caching; use Redis (Remote Dictionary Server) in production for shared, persistent caching.
Quick Quiz
1.What HTTP status code does a rate limiter return when a client exceeds the allowed request limit?
2.What does the Cache-Control: no-store directive tell browsers and CDNs?
3.What is the key advantage of Redis (Remote Dictionary Server) over node-cache for server-side caching?
4.Why should you apply a stricter rate limit to authentication endpoints compared to general API routes?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx