Introduction to Service Availability
As we've explored various security topics, we've focused primarily on protecting data and preventing unauthorized access. Today, we shift our focus to another critical aspect of security: ensuring your application remains available and responsive, even under heavy load or malicious traffic conditions.
Service availability is a crucial part of the CIA triad (Confidentiality, Integrity, Availability) that forms the foundation of information security. No matter how secure your data is, if users can't access your service, your application has failed in its purpose.
The Business Impact of Downtime
Service outages have serious business implications:
- Revenue Loss: E-commerce sites can lose thousands of dollars per minute of downtime
- User Trust: Repeated outages damage user confidence and loyalty
- SEO Impact: Frequent outages can negatively affect search engine rankings
- Competitive Disadvantage: Users may switch to competitors if your service is unreliable
- SLA Breaches: For B2B services, outages can violate service level agreements
Real-World Example: Amazon's 2018 Prime Day Outage
During Amazon's 2018 Prime Day, the site experienced intermittent outages for several hours. Analysts estimated the company lost between $72 million and $99 million in sales during this period. The outage was reportedly caused by insufficient server capacity to handle the traffic spike, not by a DDoS attack. This illustrates how even routine high traffic can cause availability issues when proper traffic management isn't in place.
Understanding Common Traffic Challenges
Before diving into solutions, it's important to understand the different types of traffic challenges your application might face:
Normal Traffic Spikes
Not all high-traffic situations are malicious. Many legitimate scenarios can cause traffic surges:
- Marketing Campaigns: A successful ad campaign or social media post going viral
- Flash Sales: Limited-time promotions causing a rush of users
- Product Launches: New feature releases driving increased interest
- Seasonal Peaks: Holiday shopping, tax season for financial apps, etc.
- News Events: Sudden interest driven by current events
API Abuse and Scraping
Some high-volume traffic comes from automated systems that may not be malicious but can still overwhelm your services:
- Aggressive Scraping: Bots collecting your data too quickly
- Poorly Implemented Clients: Third-party apps making excessive API calls
- Broken Clients: Applications stuck in retry loops
- Parallel Processing: Systems making many simultaneous requests
Intentional Denial of Service
Sometimes traffic is deliberately malicious, designed to overwhelm your services:
- Layer 3/4 Attacks: Network/transport layer flooding
- Layer 7 Attacks: Application layer attacks targeting specific endpoints
- Resource Exhaustion: Exploiting expensive operations in your application
- Distributed Attacks: Coordinated traffic from many sources (DDoS)
Rate Limiting: Concepts and Strategies
Rate limiting is the practice of controlling the amount of requests a user or client can make to your API or application within a given time period. It's a crucial tool for ensuring fair resource usage and protecting against various traffic-related issues.
Key Concepts in Rate Limiting
- Rate: The number of allowed requests per time window
- Window: The time period over which requests are counted (e.g., per second, minute, hour)
- Identifier: How you identify the client for rate limiting (IP, API key, user ID)
- Response: What happens when limits are exceeded (429 status, blocking, throttling)
- Headers: Information returned to clients about their rate limit status
Rate Limiting Algorithms
Several algorithms can be used to implement rate limiting, each with different characteristics:
Fixed Window Counter
The simplest approach divides time into fixed intervals (e.g., 1-minute windows) and counts requests in each window.
// Conceptual implementation of Fixed Window algorithm
class FixedWindowRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.clients = new Map();
}
isAllowed(clientId) {
const now = Date.now();
const windowStart = Math.floor(now / this.windowMs) * this.windowMs;
if (!this.clients.has(clientId) || this.clients.get(clientId).windowStart !== windowStart) {
// New window for this client
this.clients.set(clientId, {
windowStart,
count: 1
});
return true;
}
const client = this.clients.get(clientId);
if (client.count < this.maxRequests) {
// Increment counter
client.count++;
return true;
}
return false; // Rate limit exceeded
}
}
Drawback: This approach can allow twice the rate limit across a boundary. For example, with a limit of 100 requests per minute, a client could make 100 requests at 10:59:59 and another 100 requests at 11:00:00, getting 200 requests in a 2-second period.
Sliding Window Log
This algorithm stores a timestamp for each request and counts requests within the sliding time window.
// Conceptual implementation of Sliding Window Log algorithm
class SlidingWindowRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.clients = new Map();
}
isAllowed(clientId) {
const now = Date.now();
const windowStart = now - this.windowMs;
if (!this.clients.has(clientId)) {
this.clients.set(clientId, [now]);
return true;
}
const requests = this.clients.get(clientId);
// Remove timestamps outside the current window
while (requests.length > 0 && requests[0] <= windowStart) {
requests.shift();
}
if (requests.length < this.maxRequests) {
requests.push(now);
return true;
}
return false; // Rate limit exceeded
}
}
Advantage: Provides precise control but can use more memory as it needs to store timestamps for each request.
Token Bucket
The token bucket algorithm provides tokens at a steady rate and allows for controlled bursts of traffic.
// Conceptual implementation of Token Bucket algorithm
class TokenBucketRateLimiter {
constructor(bucketSize, refillRate) {
this.bucketSize = bucketSize;
this.refillRate = refillRate; // tokens per millisecond
this.clients = new Map();
}
isAllowed(clientId, tokens = 1) {
const now = Date.now();
if (!this.clients.has(clientId)) {
// New client gets a full bucket
this.clients.set(clientId, {
tokens: this.bucketSize - tokens,
lastRefill: now
});
return true;
}
const client = this.clients.get(clientId);
// Refill tokens based on time elapsed
const timeElapsed = now - client.lastRefill;
const tokensToAdd = timeElapsed * this.refillRate;
client.tokens = Math.min(client.tokens + tokensToAdd, this.bucketSize);
client.lastRefill = now;
if (client.tokens >= tokens) {
client.tokens -= tokens;
return true;
}
return false; // Not enough tokens
}
}
Advantage: Handles bursts well while maintaining long-term rate limits.
Rate Limiting in Express.js
In Express.js applications, you can implement rate limiting using the popular express-rate-limit middleware:
Basic Rate Limiting
const rateLimit = require('express-rate-limit');
const app = express();
// Apply rate limiting to all requests
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: 'Too many requests from this IP, please try again after 15 minutes'
});
app.use(limiter);
Route-Specific Rate Limiting
// Apply stricter rate limiting to login endpoint
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 login attempts per hour
message: 'Too many login attempts, please try again after an hour'
});
app.post('/login', loginLimiter, (req, res) => {
// Login logic
});
// Apply different rate limits to API vs web pages
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: 'Too many requests' }
});
app.use('/api/', apiLimiter);
Advanced Rate Limiting Strategies
Beyond basic rate limiting, consider these advanced strategies:
Dynamic Rate Limits
// Rate limit based on user tier
const userTierLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: (req, res) => {
// Get user tier from authenticated request
if (req.user && req.user.tier === 'premium') {
return 1000; // Premium users get higher limits
}
return 100; // Regular users get standard limits
},
keyGenerator: (req) => {
// Use user ID instead of IP for authenticated users
return req.user ? req.user.id : req.ip;
}
});
Rate Limiting with Redis for Distributed Systems
const Redis = require('ioredis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redis = new Redis({
host: 'redis-server',
port: 6379
});
const rateLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'middleware',
points: 10, // Number of points
duration: 1, // Per second
});
app.use(async (req, res, next) => {
try {
// Consume 1 point per request
await rateLimiter.consume(req.ip);
next();
} catch (err) {
res.status(429).send('Too Many Requests');
}
});
Real-World Example: GitHub API Rate Limiting
GitHub's REST API uses sophisticated rate limiting to ensure availability while allowing different usage patterns. Unauthenticated requests are limited to 60 per hour by IP address. Authenticated requests get 5,000 requests per hour per user. GitHub also implements a secondary rate limit to prevent abuse patterns, temporarily restricting actions when too many requests come in a short time. Their headers clearly communicate current limits, remaining requests, and reset times, helping developers build applications that respect these constraints.
DDoS Protection Strategies
Distributed Denial of Service (DDoS) attacks are coordinated attempts to make a service unavailable by overwhelming it with traffic from multiple sources. While rate limiting helps with certain types of DDoS attacks, comprehensive protection requires a multi-layered approach.
Types of DDoS Attacks
- Volumetric Attacks: Overwhelm bandwidth with massive traffic (e.g., UDP floods)
- Protocol Attacks: Exploit weaknesses in network protocols (e.g., SYN floods)
- Application Layer Attacks: Target specific web application functions (e.g., HTTP floods, Slowloris)
- Amplification Attacks: Use a small request to generate a large response (e.g., DNS amplification)
Defense in Depth
Effective DDoS protection requires multiple layers of defense:
Network and Infrastructure Level Protection
These strategies operate at the lower levels of the network stack:
- Increase Bandwidth: Having excess capacity can absorb some attacks
- Anycast Network: Distribute traffic across multiple global locations
- BGP FlowSpec: Filtering malicious traffic at the network level
- Load Balancing: Distribute traffic across multiple servers
- Traffic Scrubbing: Filter traffic through cleaning centers that remove malicious packets
Server Level Protection
These measures focus on server configuration:
- Connection Timeouts: Reduce the impact of slow connection attacks
- SYN Cookies: Mitigate SYN flood attacks
- Resource Limits: Cap resources any single client can consume
- Worker Processes: Use appropriate server architecture (e.g., non-blocking I/O)
Nginx Configuration for DDoS Mitigation
http {
# Basic settings
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 65s;
send_timeout 10s;
# Limit connections per IP
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_conn conn_limit_per_ip 20;
# Limit requests per IP
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=5r/s;
# Buffer size limitations
client_body_buffer_size 200K;
client_header_buffer_size 2k;
large_client_header_buffers 2 1k;
# Protect against slow HTTP attacks
reset_timedout_connection on;
server {
listen 80;
server_name example.com;
# Apply rate limiting to specific endpoints
location /login {
limit_req zone=req_limit_per_ip burst=5 nodelay;
# login handler
}
# Other configurations...
}
}
Application Level Protection
These strategies operate within your application code:
Express.js DDoS Protection Strategies
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const app = express();
// Set security headers
app.use(helmet());
// Body size limits
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
// Rate limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', apiLimiter);
// Speed limiting (gradually slows down responses when approaching rate limit)
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50,
delayMs: 500 // 0.5 seconds
});
app.use('/api/', speedLimiter);
// Implement CAPTCHA for sensitive operations
app.post('/login', verifyCaptcha, (req, res) => {
// Login logic
});
// Timeout for long operations
app.get('/search', (req, res) => {
const searchTimeout = setTimeout(() => {
res.status(503).send('Search operation timed out');
}, 5000);
performSearch(req.query)
.then(results => {
clearTimeout(searchTimeout);
res.json(results);
})
.catch(err => {
clearTimeout(searchTimeout);
res.status(500).send('Error performing search');
});
});
// Function to verify the CAPTCHA
function verifyCaptcha(req, res, next) {
if (!req.body.captchaToken) {
return res.status(400).send('CAPTCHA verification required');
}
// Verify with CAPTCHA provider
verifyCaptchaToken(req.body.captchaToken)
.then(valid => {
if (valid) next();
else res.status(400).send('CAPTCHA verification failed');
})
.catch(() => res.status(500).send('Error verifying CAPTCHA'));
}
Bot Detection and Management
Many DDoS attacks use automated bots, so detecting and filtering bot traffic is essential:
- Browser Fingerprinting: Identify clients with inconsistent browser characteristics
- Behavioral Analysis: Monitor for unusual patterns in how clients interact with your application
- JavaScript Challenges: Require clients to execute JavaScript before accessing content
- CAPTCHA: Challenge suspected bots with human verification
- Cookie/Token Validation: Require and validate cookies or tokens with each request
Simple Bot Detection with JavaScript Challenge
app.use((req, res, next) => {
// Check if client has a valid verification token
const clientToken = req.cookies.verificationToken;
if (isValidToken(clientToken)) {
// Token is valid, proceed
return next();
}
// No valid token, serve JavaScript challenge page
res.send(`
<html>
<head><title>Verification</title></head>
<body>
<p>Please wait while we verify your browser...</p>
<script>
// Generate a token based on browser characteristics
function generateToken() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const browserData = {
userAgent: navigator.userAgent,
language: navigator.language,
resolution: \`\${screen.width}x\${screen.height}\`,
webglRenderer: gl ? gl.getParameter(gl.RENDERER) : null,
timezone: new Date().getTimezoneOffset()
};
// This would actually hash the data in a real implementation
return btoa(JSON.stringify(browserData));
}
// Set token cookie and redirect back to original URL
const token = generateToken();
document.cookie = \`verificationToken=\${token}; path=/\`;
window.location.reload();
</script>
</body>
</html>
`);
});
function isValidToken(token) {
if (!token) return false;
try {
// Decode token and check if it's valid
// In a real implementation, you'd verify it against a stored value or signature
const decodedData = JSON.parse(atob(token));
// Basic validation: check if required fields exist
return decodedData.userAgent && decodedData.resolution;
} catch (e) {
return false;
}
}
Using Cloud Services for DDoS Protection
For comprehensive protection, especially against large-scale attacks, cloud-based DDoS protection services are invaluable:
- Content Delivery Networks (CDNs): Services like Cloudflare, Akamai, and Fastly
- Cloud WAFs: AWS WAF, Azure WAF, Google Cloud Armor
- Specialized DDoS Protection: Imperva, Radware, Arbor Networks
Real-World Example: Cloudflare vs. Record DDoS Attack
In February 2024, Cloudflare reported mitigating one of the largest HTTP DDoS attacks ever recorded. The attack peaked at 704 million requests per second (rps), targeting a gaming company's website. The attack used a botnet of over 255,000 unique IP addresses across 172 countries. Despite the enormous scale, Cloudflare's distributed network was able to absorb and filter the attack traffic, keeping the target website online and responsive. This example highlights the effectiveness of cloud-based DDoS protection services against attacks that would overwhelm most self-hosted defenses.
Implementing a Defense Strategy
Let's build a comprehensive defense strategy for a typical web application, combining rate limiting with DDoS protection:
Multi-Layered Defense Implementation
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const Redis = require('ioredis');
const cookieParser = require('cookie-parser');
const app = express();
// Connect to Redis for distributed rate limiting
const redis = new Redis(process.env.REDIS_URL);
// Parse cookies for token validation
app.use(cookieParser());
// Set security headers
app.use(helmet());
// Body size limits
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
// ---------- Bot Detection Middleware ----------
app.use((req, res, next) => {
// Skip bot detection for static assets
if (req.path.match(/\.(css|js|jpg|png|gif|ico)$/)) {
return next();
}
// Verify client token
const clientToken = req.cookies.clientToken;
if (isValidToken(clientToken)) {
return next();
}
// Check if this is an API request
if (req.path.startsWith('/api/')) {
return res.status(403).json({ error: 'Access denied' });
}
// Serve challenge page for other routes
serveJavaScriptChallenge(res);
});
// ---------- Global Rate Limiting ----------
// Basic rate limit by IP
const globalLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 500, // 500 requests per 5 minutes
standardHeaders: true,
message: 'Too many requests, please try again later'
});
app.use(globalLimiter);
// Speed limiter for all routes
const speedLimiter = slowDown({
windowMs: 5 * 60 * 1000,
delayAfter: 300, // Allow 300 requests per 5 minutes at full speed
delayMs: (hits) => hits * 50 // Add 50ms delay per request over limit
});
app.use(speedLimiter);
// ---------- Advanced Rate Limiting for Sensitive Routes ----------
// Create a more sophisticated rate limiter for login
const loginLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'login_limit',
points: 5, // 5 attempts
duration: 60 * 60, // Per hour
blockDuration: 60 * 60, // Block for 1 hour after exceeding
});
// Apply to login route
app.post('/api/login', async (req, res, next) => {
const key = req.ip;
try {
await loginLimiter.consume(key);
// Continue to login logic
next();
} catch (err) {
if (err.msBeforeNext) {
const minutesBeforeNext = Math.ceil(err.msBeforeNext / 60000);
res.set('Retry-After', String(minutesBeforeNext * 60));
res.status(429).json({
error: 'Too many login attempts',
retryAfter: `${minutesBeforeNext} minutes`
});
} else {
res.status(500).json({ error: 'Internal error' });
}
}
});
// ---------- API-Specific Rate Limits ----------
// Rate limiter for search endpoint (which might be resource-intensive)
const searchLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'search_limit',
points: 20, // 20 searches
duration: 60, // Per minute
});
app.get('/api/search', async (req, res, next) => {
// Get user ID or IP address
const key = req.user ? req.user.id : req.ip;
try {
// Consume more points for complex searches
const complexity = calculateSearchComplexity(req.query);
await searchLimiter.consume(key, complexity);
// Set timeout for long-running searches
const searchTimeout = setTimeout(() => {
res.status(503).send('Search operation timed out');
}, 5000);
// Perform the search
const results = await performSearch(req.query);
clearTimeout(searchTimeout);
res.json(results);
} catch (err) {
if (err.msBeforeNext) {
res.status(429).json({
error: 'Search rate limit exceeded',
retryAfter: Math.ceil(err.msBeforeNext / 1000)
});
} else {
res.status(500).json({ error: 'Search error' });
}
}
});
// ---------- User-Tier Based Rate Limiting ----------
// Differentiated rate limits based on user tier
app.use('/api/premium', async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const key = req.user.id;
const tier = req.user.tier || 'free';
// Different limits based on user tier
const tierLimits = {
free: { points: 50, duration: 3600 },
basic: { points: 200, duration: 3600 },
premium: { points: 1000, duration: 3600 },
enterprise: { points: 5000, duration: 3600 }
};
const limit = tierLimits[tier] || tierLimits.free;
try {
// Create or get limiter for this tier
const tierLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: `tier_${tier}`,
points: limit.points,
duration: limit.duration
});
await tierLimiter.consume(key);
next();
} catch (err) {
if (err.msBeforeNext) {
res.status(429).json({
error: `Rate limit exceeded for ${tier} tier`,
retryAfter: Math.ceil(err.msBeforeNext / 1000),
upgradeUrl: tier !== 'enterprise' ? '/account/upgrade' : null
});
} else {
res.status(500).json({ error: 'Internal error' });
}
}
});
// ---------- Helper Functions ----------
function isValidToken(token) {
// Implement token validation logic
// In production, you would verify signatures, timestamps, etc.
if (!token) return false;
try {
const decoded = JSON.parse(Buffer.from(token, 'base64').toString());
return decoded.userAgent && Date.now() - decoded.timestamp < 86400000; // Valid for 24 hours
} catch (e) {
return false;
}
}
function serveJavaScriptChallenge(res) {
// In production, this would be more sophisticated
res.send(`
<html>
<head><title>Verification</title></head>
<body>
<p>Please wait while we verify your browser...</p>
<script>
const token = btoa(JSON.stringify({
userAgent: navigator.userAgent,
timestamp: Date.now()
}));
document.cookie = \`clientToken=\${token}; path=/; max-age=86400\`;
window.location.reload();
</script>
</body>
</html>
`);
}
function calculateSearchComplexity(query) {
// Calculate how resource-intensive a search might be
let complexity = 1;
// More fields = more complex
complexity += Object.keys(query).length * 0.5;
// Wildcard searches are more expensive
if (query.q && query.q.includes('*')) {
complexity += 2;
}
// Large date ranges are expensive
if (query.startDate && query.endDate) {
const start = new Date(query.startDate);
const end = new Date(query.endDate);
const daysDiff = Math.abs((end - start) / (1000 * 60 * 60 * 24));
complexity += Math.min(daysDiff / 30, 5); // Max 5 points for date range
}
return Math.max(1, Math.ceil(complexity));
}
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Monitoring and Response Planning
Even with the best preventative measures, you need to monitor for attacks and have a response plan:
Monitoring Traffic Patterns
- Request Rate Monitoring: Track requests per second across endpoints
- Resource Utilization: Monitor CPU, memory, network, and database load
- Error Rate Tracking: Watch for spikes in 4xx and 5xx responses
- Geographic Anomalies: Identify unusual traffic sources by country
- Client Fingerprinting: Detect unusual patterns in user agents or device characteristics
Building an Incident Response Plan
- Detection: Set up alerts for traffic anomalies and threshold breaches
- Classification: Determine the type and severity of the attack
- Containment: Implement emergency measures to mitigate the impact
- Communication: Notify stakeholders and, if necessary, users
- Eradication: Block attack sources and implement additional protections
- Recovery: Restore normal operations and monitor for recurring issues
- Post-Mortem: Analyze what happened and improve defenses
Emergency Response Techniques
- IP Blocking: Temporarily block suspicious IP ranges
- Geo-Blocking: Restrict traffic from countries where attacks originate
- Stricter Rate Limits: Reduce rate limits during attacks
- Feature Degradation: Disable non-essential features to reduce load
- Circuit Breaking: Temporarily disable vulnerable endpoints
- Cloudflare "Under Attack" Mode: If using Cloudflare, enable their advanced protection
Dynamic Defense Adjustments
// System to dynamically adjust rate limits based on server load
const os = require('os');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
// Monitor system load every 5 seconds
setInterval(async () => {
// Check current system load
const currentLoad = os.loadavg()[0]; // 1-minute load average
const cpuCount = os.cpus().length;
const loadPerCPU = currentLoad / cpuCount;
// Set rate limit policy based on load
let ratePolicy = 'normal';
if (loadPerCPU > 0.7 && loadPerCPU <= 0.85) {
ratePolicy = 'moderate';
} else if (loadPerCPU > 0.85) {
ratePolicy = 'aggressive';
}
// Store current policy in Redis for rate limiters to use
await redis.set('rate_limit_policy', ratePolicy);
// Log policy changes
console.log(`Load: ${loadPerCPU.toFixed(2)}, Policy: ${ratePolicy}`);
}, 5000);
// Middleware to apply dynamic rate limits
app.use(async (req, res, next) => {
// Skip for certain endpoints
if (req.path.match(/\.(css|js|jpg|png|gif|ico)$/)) {
return next();
}
// Get current rate limit policy
const policy = await redis.get('rate_limit_policy') || 'normal';
// Apply different limits based on policy
const limits = {
normal: { windowMs: 60000, max: 100 },
moderate: { windowMs: 60000, max: 50 },
aggressive: { windowMs: 60000, max: 20 }
};
const currentLimit = limits[policy];
// Apply rate limiting
const limiter = rateLimit({
windowMs: currentLimit.windowMs,
max: currentLimit.max,
message: `Server is experiencing high load. Please try again later.`
});
return limiter(req, res, next);
});
Real-World Example: GitHub's Response to the 2018 Memcached DDoS
In February 2018, GitHub experienced the largest DDoS attack ever recorded at that time, peaking at 1.35 Tbps using a Memcached amplification attack. GitHub's response was exemplary: their monitoring systems detected the attack within minutes, they automatically activated their DDoS mitigation service (Akamai Prolexic), and traffic was rerouted through scrubbing centers to filter out malicious packets. Within ten minutes of detection, the attack was mitigated, and GitHub remained accessible to legitimate users. This demonstrates the importance of both monitoring and having an automated response plan in place.
Best Practices
Rate Limiting Best Practices
- Be Clear About Limits: Communicate rate limits through headers
- Account for Distributed Systems: Use Redis or another shared store
- Progressive Throttling: Gradually slow down rather than hard cutoffs
- Different Limits for Different Resources: Apply stricter limits to expensive operations
- User-Based Limits: Use user IDs instead of IPs when possible
- Dynamic Adjustment: Adapt limits based on server load
- Include Retry-After Headers: Tell clients when they can try again
DDoS Mitigation Best Practices
- Multiple Layers of Defense: Combine network, server, and application protections
- CDN Utilization: Leverage CDNs to absorb traffic and filter attacks
- Minimize Attack Surface: Close unnecessary ports and disable unused services
- Resource Quotas: Limit resources any single user/request can consume
- Regularly Test Defenses: Conduct simulated DDoS attacks to verify protections
- Monitor and Alert: Set up real-time monitoring for unusual traffic patterns
- Know Your Baseline: Understand normal traffic patterns to spot anomalies
- Comprehensive Incident Response Plan: Prepare for attacks before they happen
Common Anti-Patterns to Avoid
- IP-Only Rate Limiting: Can affect multiple users behind NATs or proxies
- Hard-Coded Rate Limits: Not adaptable to changing traffic patterns
- Excessive Memory Usage: Storing too much data for rate limiting
- Inadequate Logging: Not capturing enough information to analyze attacks
- Reactive-Only Approach: Only responding to attacks instead of preparing
- Single-Layer Defense: Relying on only one protection mechanism
- Ignoring False Positives: Blocking legitimate traffic unnecessarily
Practical Exercises
Exercise 1: Implement Basic Rate Limiting
Create an Express.js application with the following rate limiting features:
- Global rate limit of 100 requests per minute
- A stricter rate limit for login attempts (5 per hour)
- Custom response messages for when limits are exceeded
- Proper rate limit headers in responses
Exercise 2: Dynamic Rate Limiting
Extend your rate limiting implementation to include:
- Different rate limits for authenticated vs. unauthenticated users
- Tiered rate limits based on user roles (e.g., admin, premium, basic)
- Dynamic adjustment of limits based on server load
Exercise 3: Basic DDoS Protection
Implement basic DDoS protection measures:
- Request timeout limitations
- Body size limits
- A simple IP blacklisting system
- Browser fingerprinting to detect suspicious clients
Exercise 4: Build a Response Plan
Create a comprehensive incident response plan for a DDoS attack:
- Define detection metrics and thresholds
- Document immediate response steps for different attack types
- Create a communication template for stakeholders
- Design a post-mortem process for improving defenses
Additional Resources
Next Class Preparation
For our next session on Full Stack Application Development, please:
- Review the security concepts we've covered so far
- Think about how these security measures integrate into a complete application
- Prepare questions about implementing security in real-world projects