Defend Your Node.js API: Stop AI Bots, Ddos & Cost Attacks

Introduction
Your server logs show 10,000 login attempts in the last hour. Your AWS bill jumped from $300 to $4,500 overnight. Someone is hammering your password reset endpoint, sending spam emails to random addresses through your system.
This is what modern attacks look like in 2026. AI-powered bots don't behave like the clunky scripts from five years ago. They mimic real users perfectly—random timing, realistic mouse movements, rotating IP addresses across residential proxies. Traditional defenses like simple rate limiting or CAPTCHAs are nearly useless against them.
Here's the hard truth: a single compromised API endpoint can bankrupt a startup in days. One unprotected route can send a million spam emails, destroying your domain's reputation forever. One missing rate limit can let attackers burn through your entire AI API budget in hours.
In this guide, you'll learn practical defense strategies that work against modern AI attacks. No theory, no buzzwords—just real code you can deploy today to protect your Node.js backend. We'll cover intelligent rate limiting, behavioral fingerprinting, cost controls, and automated threat detection. By the end, you'll have a battle-tested security layer that stops attacks while keeping legitimate users happy.
Why Traditional Security Fails Against AI Attacks
Let's start with why your current security probably isn't good enough.
The Old Way vs The AI Way
Traditional bot (2020):
- •Same IP address for all requests
- •Predictable timing (exactly 1 request per second)
- •Generic user agent string
- •Fails at basic CAPTCHA
- •Easy to detect and block
AI-powered bot (2026):
- •Rotates through thousands of residential IP addresses
- •Random timing with human-like patterns (2.3s, 1.8s, 3.1s delays)
- •Unique browser fingerprints for each "session"
- •Solves CAPTCHAs with 95% accuracy using computer vision
- •Adapts behavior when it detects security measures
Real Attack Scenarios You Need to Defend Against
1. Credential Stuffing: Attackers use AI to test millions of stolen email/password combinations from data breaches. They target your login endpoint, spreading requests across many IPs to avoid detection. One successful login gives them access to a real user account.
2. API Cost Exploitation: Your AI chat endpoint costs you $0.05 per request (GPT-4 API calls). An attacker finds it, writes a script, and makes 100,000 requests before you wake up. That's $5,000 gone.
3. Email/SMS Spam: Bots create thousands of fake accounts, each triggering verification emails. Your SMTP provider marks you as spam. Now your legitimate password reset emails don't reach real users.
4. Inventory Scalping: You're selling limited edition sneakers. Bots buy all 500 pairs in 3 seconds, before real customers even load the page. Those bots resell them for 10x the price.
5. Data Scraping: Competitors use AI to systematically download your entire product catalog, pricing, and user reviews. They rebuild your database and undercut your prices.
Why This Matters to Your Business
Security isn't just about preventing hacks. It's about:
- •Protecting your money: AWS charges for CPU, memory, database queries, and API calls. Attacks cost real money.
- •Maintaining service quality: Attack traffic slows down your servers, making the experience terrible for real users.
- •Preserving your reputation: If your platform sends spam or gets hacked, users lose trust forever.
- •Staying compliant: GDPR, CCPA, and other regulations require you to protect user data. Breaches mean lawsuits and fines.
Now let's build defenses that actually work.
Smart Rate Limiting: Your First Line of Defense
Rate limiting controls how many requests someone can make in a time period. Simple, but most developers do it wrong.
Why Basic Rate Limiting Isn't Enough
// ❌ BAD: This is too easy to bypass
const rateLimit = {};
app.use((req, res, next) => {
const ip = req.ip;
const now = Date.now();
if (!rateLimit[ip]) rateLimit[ip] = [];
// Remove old requests
rateLimit[ip] = rateLimit[ip].filter(time => now - time < 60000);
if (rateLimit[ip].length > 100) {
return res.status(429).json({ error: 'Too many requests' });
}
rateLimit[ip].push(now);
next();
});Problems with this approach:
- •Stores everything in memory (crashes when restarted)
- •Attackers just switch IP addresses every 100 requests
- •Same limit for everyone (penalizes legitimate power users)
- •Fixed time window (allows burst attacks at window boundaries)
Better Approach: Redis-Backed Sliding Window
import { createClient } from 'redis';
// Connect to Redis (in-memory database)
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// Sliding window rate limiter
async function checkRateLimit(identifier, maxRequests, windowMs) {
const key = `ratelimit:${identifier}`;
const now = Date.now();
const windowStart = now - windowMs;
// Start a Redis pipeline (multiple commands in one roundtrip)
const pipeline = redis.multi();
// Remove requests older than our window
pipeline.zRemRangeByScore(key, 0, windowStart);
// Count remaining requests in window
pipeline.zCard(key);
// Add current request with timestamp
pipeline.zAdd(key, { score: now, value: `${now}-${Math.random()}` });
// Auto-expire the key after window period
pipeline.expire(key, Math.ceil(windowMs / 1000));
// Execute all commands together
const results = await pipeline.exec();
const requestCount = results[1]; // Count from zCard
return {
allowed: requestCount < maxRequests,
remaining: Math.max(0, maxRequests - requestCount),
resetAt: new Date(now + windowMs)
};
}
// Apply rate limiting middleware
app.use(async (req, res, next) => {
// Use IP address as identifier (we'll improve this later)
const identifier = req.ip;
const limit = await checkRateLimit(identifier, 100, 60000); // 100 req/min
// Set standard rate limit headers
res.setHeader('X-RateLimit-Limit', 100);
res.setHeader('X-RateLimit-Remaining', limit.remaining);
res.setHeader('X-RateLimit-Reset', limit.resetAt.toISOString());
if (!limit.allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil((limit.resetAt - Date.now()) / 1000)
});
}
next();
});Why this works better:
- •Survives server restarts (data in Redis, not memory)
- •True sliding window (no burst attacks at boundaries)
- •Fast performance (Redis is in-memory)
- •Can share across multiple servers
Different Limits for Different Situations
Not all endpoints are equal. Your homepage can handle more traffic than your AI chat endpoint.
// Helper function for flexible rate limiting
async function createRateLimiter(config) {
return async (req, res, next) => {
const identifier = config.getIdentifier(req);
const limit = await checkRateLimit(
identifier,
config.maxRequests,
config.windowMs
);
if (!limit.allowed) {
return res.status(429).json({
error: config.message || 'Too many requests',
retryAfter: Math.ceil((limit.resetAt - Date.now()) / 1000)
});
}
next();
};
}
// Apply different limits to different routes
const ipLimiter = await createRateLimiter({
maxRequests: 100,
windowMs: 60000, // 1 minute
getIdentifier: (req) => req.ip,
message: 'Too many requests from this IP'
});
const loginLimiter = await createRateLimiter({
maxRequests: 5,
windowMs: 300000, // 5 minutes
getIdentifier: (req) => req.body.email || req.ip,
message: 'Too many login attempts. Try again in 5 minutes.'
});
const aiLimiter = await createRateLimiter({
maxRequests: 10,
windowMs: 3600000, // 1 hour
getIdentifier: (req) => req.user?.id || req.ip,
message: 'AI quota exceeded. Limit resets in 1 hour.'
});
// Use them on specific routes
app.use('/', ipLimiter); // Global limit
app.post('/api/login', loginLimiter); // Strict limit for login
app.post('/api/ai/chat', aiLimiter); // Strict limit for expensive AI callsKey insight: Attackers love expensive operations. Put stricter limits on anything that costs you money or server resources.
Tracking User Behavior: Spot the Bots
Rate limiting stops fast attacks. But smart bots stay under your limits. You need to look at how users behave, not just how fast.
What Makes Human Behavior Different from Bots?
Humans are wonderfully messy. Bots are suspiciously perfect.
Human patterns:
- •Inconsistent timing (2.1s, 5.3s, 1.8s between clicks)
- •Navigate logically (homepage → product → checkout)
- •Make mistakes (typos, back button, reload page)
- •Take breaks (idle time, mouse movements)
Bot patterns:
- •Perfect timing (exactly 2.0s, 2.0s, 2.0s)
- •Unnatural navigation (direct URLs, no referrer)
- •No mistakes (perfect forms, no retries)
- •No idle time (continuous requests)
Collecting Behavioral Signals
// Middleware to track request patterns
async function trackBehavior(req, res, next) {
const sessionKey = `behavior:${req.user?.id || req.ip}`;
// Get recent request history
const history = await redis.lRange(sessionKey, 0, 19); // Last 20 requests
// Create current request record
const currentRequest = {
path: req.path,
method: req.method,
timestamp: Date.now(),
userAgent: req.get('user-agent'),
referer: req.get('referer'),
};
// Store in Redis (keep last 20 requests)
await redis.lPush(sessionKey, JSON.stringify(currentRequest));
await redis.lTrim(sessionKey, 0, 19);
await redis.expire(sessionKey, 3600); // Expire after 1 hour
// Analyze behavior (if we have enough history)
if (history.length >= 5) {
const requests = history.map(h => JSON.parse(h));
const analysis = analyzeBehavior(requests, currentRequest);
// Attach analysis to request for later use
req.behaviorAnalysis = analysis;
}
next();
}
// Analyze request patterns for bot-like behavior
function analyzeBehavior(history, current) {
const suspicionReasons = [];
let suspicionScore = 0;
// Check 1: Perfect timing (bots are too consistent)
const intervals = history.slice(0, 10).map((req, i) => {
if (i === 0) return null;
return req.timestamp - history[i - 1].timestamp;
}).filter(Boolean);
if (intervals.length >= 5) {
// Calculate variance in timing
const avg = intervals.reduce((a, b) => a + b) / intervals.length;
const variance = intervals.reduce((sum, val) =>
sum + Math.pow(val - avg, 2), 0
) / intervals.length;
// Low variance = robotic behavior
if (variance < 50 && avg < 3000) {
suspicionScore += 30;
suspicionReasons.push('Robotic request timing');
}
}
// Check 2: Sequential URL patterns (scraping behavior)
const paths = history.map(h => h.path);
const hasSequentialPattern = detectSequentialAccess(paths);
if (hasSequentialPattern) {
suspicionScore += 40;
suspicionReasons.push('Sequential page access detected');
}
// Check 3: No referer on deep pages (direct URL access)
const isDeepPage = current.path.split('/').length > 3;
if (isDeepPage && !current.referer) {
suspicionScore += 15;
suspicionReasons.push('Direct access to deep page');
}
// Check 4: User agent switching
const userAgents = new Set(history.map(h => h.userAgent));
if (userAgents.size > 2) {
suspicionScore += 25;
suspicionReasons.push('Multiple user agents in session');
}
return {
suspicionScore,
suspicionReasons,
isSuspicious: suspicionScore >= 50
};
}
// Helper: Detect if someone is accessing URLs in sequential order
function detectSequentialAccess(paths) {
// Look for patterns like /products/1, /products/2, /products/3
const numbers = paths
.map(path => {
const match = path.match(/\/(\d+)$/);
return match ? parseInt(match[1]) : null;
})
.filter(n => n !== null);
if (numbers.length < 5) return false;
// Check if numbers are mostly sequential
let sequential = 0;
for (let i = 1; i < numbers.length; i++) {
if (numbers[i] === numbers[i - 1] + 1) sequential++;
}
return sequential / numbers.length > 0.6; // 60% sequential
}
// Use the behavior tracker
app.use(trackBehavior);
// Take action based on suspicion score
app.use((req, res, next) => {
if (req.behaviorAnalysis?.isSuspicious) {
// Log suspicious activity
console.warn('Suspicious behavior:', {
ip: req.ip,
userId: req.user?.id,
score: req.behaviorAnalysis.suspicionScore,
reasons: req.behaviorAnalysis.suspicionReasons
});
// High suspicion = block immediately
if (req.behaviorAnalysis.suspicionScore > 80) {
return res.status(403).json({
error: 'Automated behavior detected'
});
}
// Medium suspicion = add challenge (CAPTCHA, email verification, etc.)
if (req.behaviorAnalysis.suspicionScore > 60) {
req.requiresChallenge = true;
}
}
next();
});Browser Fingerprinting: Know Your Users' Devices
Every browser has unique characteristics. Collect these from the frontend and send with requests.
// Frontend code: Collect device fingerprint
async function getDeviceFingerprint() {
// Canvas fingerprinting (unique rendering per device)
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('fingerprint', 2, 2);
const canvasData = canvas.toDataURL();
// Hash the canvas data
const canvasHash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(canvasData)
);
const canvasId = Array.from(new Uint8Array(canvasHash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.substring(0, 16);
return {
canvasId,
screenResolution: `${screen.width}x${screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemory: navigator.deviceMemory,
colorDepth: screen.colorDepth
};
}
// Send fingerprint with every API request
const fingerprint = await getDeviceFingerprint();
fetch('/api/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Device-Fingerprint': JSON.stringify(fingerprint)
},
body: JSON.stringify({ /* your data */ })
});// Backend: Track fingerprints
app.use(async (req, res, next) => {
const fingerprintHeader = req.get('x-device-fingerprint');
if (fingerprintHeader) {
try {
const fingerprint = JSON.parse(fingerprintHeader);
// Check if multiple accounts use same fingerprint (suspicious)
const accountsWithFingerprint = await redis.sCard(
`fingerprint:${fingerprint.canvasId}:accounts`
);
if (req.user) {
await redis.sAdd(
`fingerprint:${fingerprint.canvasId}:accounts`,
req.user.id
);
}
// Many accounts from one device = bot farm
if (accountsWithFingerprint > 5) {
req.suspicionReasons = req.suspicionReasons || [];
req.suspicionReasons.push('Multiple accounts from same device');
}
req.fingerprint = fingerprint;
} catch (err) {
// Invalid fingerprint format
}
} else {
// No fingerprint = likely bot or privacy browser
req.suspicionReasons = req.suspicionReasons || [];
req.suspicionReasons.push('Missing device fingerprint');
}
next();
});Protecting Expensive Operations: Stop Cost Attacks
Some endpoints cost you real money. Protect them aggressively.
Identify Your High-Cost Endpoints
Common expensive operations:
- •AI/ML inference: GPT-4, image generation, speech-to-text
- •Third-party APIs: SMS (Twilio), email (SendGrid), payment processing
- •Heavy computation: PDF generation, video encoding, data exports
- •Database operations: Complex aggregations, full-text search
Track and Limit Costs in Real-Time
// Define cost for each operation
const OPERATION_COSTS = {
'ai:gpt4': 0.03, // $0.03 per request (avg)
'ai:dalle': 0.10, // $0.10 per image
'sms:send': 0.0075, // $0.0075 per SMS
'email:send': 0.0001, // $0.0001 per email
'pdf:generate': 0.005 // $0.005 per PDF
};
// Track costs per user/IP
async function trackCost(identifier, operation, cost) {
const hourKey = `cost:${identifier}:${getCurrentHour()}`;
const dayKey = `cost:${identifier}:${getCurrentDay()}`;
// Increment hourly and daily costs
await redis.incrByFloat(hourKey, cost);
await redis.incrByFloat(dayKey, cost);
// Set expiration
await redis.expire(hourKey, 7200); // 2 hours
await redis.expire(dayKey, 172800); // 2 days
// Check if over budget
const hourlyCost = parseFloat(await redis.get(hourKey) || '0');
const dailyCost = parseFloat(await redis.get(dayKey) || '0');
return {
hourlyCost,
dailyCost,
overHourlyBudget: hourlyCost > 10, // $10/hour per user
overDailyBudget: dailyCost > 50 // $50/day per user
};
}
function getCurrentHour() {
const now = new Date();
return `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}-${now.getHours()}`;
}
function getCurrentDay() {
const now = new Date();
return `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
}
// Middleware to check cost budget before expensive operations
async function checkCostBudget(operation) {
return async (req, res, next) => {
const identifier = req.user?.id || req.ip;
const cost = OPERATION_COSTS[operation];
const tracking = await trackCost(identifier, operation, cost);
if (tracking.overDailyBudget) {
return res.status(429).json({
error: 'Daily budget exceeded',
currentCost: tracking.dailyCost,
limit: 50,
resetAt: new Date(Date.now() + 86400000).toISOString()
});
}
if (tracking.overHourlyBudget) {
return res.status(429).json({
error: 'Hourly budget exceeded',
currentCost: tracking.hourlyCost,
limit: 10,
resetAt: new Date(Date.now() + 3600000).toISOString()
});
}
// Store cost info in request for later use
req.costTracking = tracking;
next();
};
}
// Protect expensive AI endpoint
app.post('/api/ai/generate',
await checkCostBudget('ai:gpt4'),
async (req, res) => {
// Call OpenAI API
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: req.body.prompt }]
});
res.json(response);
}
);
// Protect SMS endpoint
app.post('/api/sms/send',
await checkCostBudget('sms:send'),
async (req, res) => {
// Send SMS via Twilio
await twilioClient.messages.create({
body: req.body.message,
to: req.body.to,
from: process.env.TWILIO_PHONE
});
res.json({ success: true });
}
);Global Cost Monitoring and Alerts
Track total infrastructure costs and alert when they spike.
// Track global costs (all users combined)
async function trackGlobalCost(operation, cost) {
const hourKey = `global:cost:${getCurrentHour()}`;
const currentCost = await redis.incrByFloat(hourKey, cost);
await redis.expire(hourKey, 7200);
// Alert if hourly cost exceeds $500
if (currentCost > 500 && currentCost - cost <= 500) {
await sendCostAlert({
message: `🚨 Global hourly cost exceeded $500`,
currentCost: currentCost.toFixed(2),
operation
});
}
return currentCost;
}
async function sendCostAlert(alert) {
// Send to Slack, email, or monitoring service
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: alert.message,
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Current Cost:* $${alert.currentCost}\n*Operation:* ${alert.operation}`
}
}]
})
});
}
// Add global tracking to expensive operations
app.post('/api/ai/generate', async (req, res) => {
const cost = OPERATION_COSTS['ai:gpt4'];
// Track globally
await trackGlobalCost('ai:gpt4', cost);
// ... rest of handler
});Defending Against Email and SMS Spam
Your notification system is a target. Protect it before attackers abuse it.
Email Verification Strategies
Not all emails are equal. Treat risky ones differently.
// Check if email is suspicious
async function analyzeEmail(email, ip) {
const domain = email.split('@')[1].toLowerCase();
const risks = [];
let riskScore = 0;
// Check 1: Disposable email service
const disposableDomains = [
'tempmail.com', 'guerrillamail.com', '10minutemail.com',
'throwaway.email', 'mailinator.com', 'trashmail.com'
];
if (disposableDomains.includes(domain)) {
riskScore += 50;
risks.push('Disposable email service');
}
// Check 2: Domain age (new domains are suspicious)
// In production, use WhoisXML API or similar
const domainAge = await checkDomainAge(domain);
if (domainAge < 30) {
riskScore += 30;
risks.push('Recently created domain');
}
// Check 3: Check if domain has valid MX records
const hasMX = await checkMXRecords(domain);
if (!hasMX) {
riskScore += 40;
risks.push('No valid email server');
}
// Check 4: Historical abuse from this domain
const abuseRate = await getEmailDomainAbuseRate(domain);
if (abuseRate > 0.3) {
riskScore += 40;
risks.push('High abuse rate from domain');
}
// Check 5: IP reputation
const ipRisk = await checkIPReputation(ip);
if (ipRisk > 0.7) {
riskScore += 30;
risks.push('Suspicious IP address');
}
return { riskScore, risks };
}
async function checkMXRecords(domain) {
const dns = require('dns').promises;
try {
const records = await dns.resolveMx(domain);
return records.length > 0;
} catch {
return false;
}
}
async function getEmailDomainAbuseRate(domain) {
// Check your database for abuse reports
const total = await db.collection('users').countDocuments({
email: new RegExp(`@${domain}$`, 'i')
});
const flagged = await db.collection('users').countDocuments({
email: new RegExp(`@${domain}$`, 'i'),
flaggedAsSpam: true
});
return total > 0 ? flagged / total : 0;
}
async function checkIPReputation(ip) {
// Use IP reputation service (IPQualityScore, etc.)
try {
const response = await fetch(
`https://ipqualityscore.com/api/json/ip/${process.env.IPQS_KEY}/${ip}`
);
const data = await response.json();
return data.fraud_score / 100; // Convert to 0-1 scale
} catch {
return 0; // Fail safe if service is down
}
}
// Registration endpoint with email analysis
app.post('/api/register', async (req, res) => {
const { email, password } = req.body;
// Analyze email risk
const analysis = await analyzeEmail(email, req.ip);
// Block high-risk emails immediately
if (analysis.riskScore > 80) {
return res.status(400).json({
error: 'Email not allowed',
reasons: analysis.risks
});
}
// Medium risk = require phone verification
const requiresPhone = analysis.riskScore > 50;
// Create user
await db.collection('users').insertOne({
email,
password: await hashPassword(password),
emailVerified: false,
phoneVerified: false,
requiresPhoneVerification: requiresPhone,
riskScore: analysis.riskScore,
createdAt: new Date()
});
// Send verification email (rate limited)
if (await canSendVerificationEmail(email)) {
await sendVerificationEmail(email);
}
res.json({
success: true,
requiresPhoneVerification: requiresPhone
});
});Rate Limit Verification Emails Strictly
// Prevent abuse of email verification system
async function canSendVerificationEmail(email) {
const dailyKey = `email:verify:${email}:${getCurrentDay()}`;
const count = await redis.incr(dailyKey);
await redis.expire(dailyKey, 86400);
// Max 3 verification emails per day per address
if (count > 3) {
return false;
}
// Global limit: max 1000 verification emails per hour
const globalKey = `email:verify:global:${getCurrentHour()}`;
const globalCount = await redis.incr(globalKey);
await redis.expire(globalKey, 3600);
return globalCount <= 1000;
}
// Same for SMS
async function canSendVerificationSMS(phone) {
const dailyKey = `sms:verify:${phone}:${getCurrentDay()}`;
const count = await redis.incr(dailyKey);
await redis.expire(dailyKey, 86400);
// Max 3 SMS per day per number
if (count > 3) {
return false;
}
// Global limit: max 500 SMS per hour (expensive!)
const globalKey = `sms:verify:global:${getCurrentHour()}`;
const globalCount = await redis.incr(globalKey);
await redis.expire(globalKey, 3600);
return globalCount <= 500;
}Credential Stuffing Defense: Protect Login Endpoints
Login pages are the #1 target for AI bots. They test stolen credentials to find valid accounts.
Multi-Layer Login Protection
// Login endpoint with multiple defenses
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const identifier = `${email}:${req.ip}`;
// Defense 1: Rate limit login attempts
const loginAttempts = await redis.incr(`login:attempts:${identifier}`);
await redis.expire(`login:attempts:${identifier}`, 900); // 15 minutes
if (loginAttempts > 5) {
return res.status(429).json({
error: 'Too many login attempts. Try again in 15 minutes.'
});
}
// Defense 2: Check if this IP is trying multiple accounts
const ipAttempts = await redis.incr(`login:ip:${req.ip}:${getCurrentHour()}`);
await redis.expire(`login:ip:${req.ip}:${getCurrentHour()}`, 3600);
if (ipAttempts > 20) {
return res.status(429).json({
error: 'Too many accounts attempted from this location'
});
}
// Defense 3: Check credentials
const user = await db.collection('users').findOne({ email });
if (!user || !(await verifyPassword(password, user.password))) {
// Track failed attempt
await redis.incr(`login:failed:${email}:${getCurrentDay()}`);
// Generic error (don't reveal if email exists)
return res.status(401).json({
error: 'Invalid email or password'
});
}
// Defense 4: Check for suspicious login patterns
const failedToday = await redis.get(`login:failed:${email}:${getCurrentDay()}`);
if (parseInt(failedToday || '0') > 10) {
// Many failed attempts before success = credential stuffing
await sendSecurityAlert(user.id, {
type: 'suspicious_login',
message: 'Multiple failed login attempts detected',
ip: req.ip
});
// Require additional verification
return res.json({
requiresVerification: true,
verificationToken: await generateVerificationToken(user.id)
});
}
// Defense 5: Device fingerprint check
const knownDevice = await isKnownDevice(user.id, req.fingerprint);
if (!knownDevice) {
// New device = send email notification
await sendNewDeviceAlert(user.email, req.fingerprint);
}
// Success: generate session token
const token = await generateSessionToken(user.id);
// Reset failed attempts counter
await redis.del(`login:failed:${email}:${getCurrentDay()}`);
res.json({ token, user: { id: user.id, email: user.email } });
});
// Check if device is recognized
async function isKnownDevice(userId, fingerprint) {
if (!fingerprint?.canvasId) return false;
const devices = await redis.sMembers(`user:${userId}:devices`);
return devices.includes(fingerprint.canvasId);
}
// Remember device after successful login
async function rememberDevice(userId, fingerprint) {
if (!fingerprint?.canvasId) return;
await redis.sAdd(`user:${userId}:devices`, fingerprint.canvasId);
}Password Reset Protection
Password reset is another attack vector. Attackers use it to spam users or take over accounts.
app.post('/api/password-reset-request', async (req, res) => {
const { email } = req.body;
// Rate limit: max 3 requests per email per day
const emailKey = `reset:${email}:${getCurrentDay()}`;
const emailCount = await redis.incr(emailKey);
await redis.expire(emailKey, 86400);
if (emailCount > 3) {
// Still return success (don't reveal if email exists)
return res.json({ success: true });
}
// Rate limit: max 5 requests per IP per hour
const ipKey = `reset:ip:${req.ip}:${getCurrentHour()}`;
const ipCount = await redis.incr(ipKey);
await redis.expire(ipKey, 3600);
if (ipCount > 5) {
return res.status(429).json({
error: 'Too many reset requests'
});
}
// Find user
const user = await db.collection('users').findOne({ email });
if (user) {
// Generate secure reset token
const token = crypto.randomBytes(32).toString('hex');
await redis.set(
`reset:token:${token}`,
user.id.toString(),
{ EX: 3600 } // Expires in 1 hour
);
// Send reset email (if global limit allows)
if (await canSendResetEmail()) {
await sendPasswordResetEmail(email, token);
}
}
// Always return success (don't reveal if email exists)
res.json({ success: true });
});
async function canSendResetEmail() {
const globalKey = `reset:global:${getCurrentHour()}`;
const count = await redis.incr(globalKey);
await redis.expire(globalKey, 3600);
return count <= 1000; // Max 1000 reset emails per hour
}IP Reputation and Blocking
Not all IP addresses are equal. Some are known sources of attacks.
Check IP Reputation in Real-Time
async function checkIPReputation(ip) {
// Check 1: Is it in our local blocklist?
const isBlocked = await redis.sIsMember('blocked:ips', ip);
if (isBlocked) {
return { blocked: true, reason: 'Previously flagged' };
}
// Check 2: Is it a datacenter IP? (bots often use cloud servers)
const isDatacenter = await isDatacenterIP(ip);
if (isDatacenter) {
return { suspicious: true, reason: 'Datacenter IP' };
}
// Check 3: External reputation check (cache results)
const cacheKey = `ip:reputation:${ip}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Call IP reputation service
try {
const response = await fetch(
`https://ipqualityscore.com/api/json/ip/${process.env.IPQS_KEY}/${ip}`
);
const data = await response.json();
const result = {
fraudScore: data.fraud_score,
isVPN: data.vpn,
isProxy: data.proxy,
isTor: data.tor,
recentAbuse: data.recent_abuse,
botStatus: data.bot_status
};
// Cache for 1 hour
await redis.set(cacheKey, JSON.stringify(result), { EX: 3600 });
return result;
} catch (err) {
// Service unavailable - fail open
return { error: true };
}
}
async function isDatacenterIP(ip) {
// Check against known datacenter ranges
// AWS, Google Cloud, Azure, DigitalOcean, etc.
const datacenterRanges = [
// Example AWS ranges (use complete list in production)
'3.0.0.0/8',
'13.0.0.0/8',
// ... more ranges
];
// Simple CIDR check (use ip-range-check library in production)
return false; // Placeholder
}
// Middleware to block/challenge suspicious IPs
app.use(async (req, res, next) => {
const reputation = await checkIPReputation(req.ip);
if (reputation.blocked) {
return res.status(403).json({
error: 'Access denied',
reason: 'IP address blocked'
});
}
if (reputation.isVPN || reputation.isProxy) {
// VPN users might be legitimate, but add friction
req.requiresChallenge = true;
}
if (reputation.fraudScore > 85) {
// Very suspicious - block
await redis.sAdd('blocked:ips', req.ip);
return res.status(403).json({
error: 'Access denied'
});
}
next();
});Automatic IP Blocking Based on Behavior
// Auto-block IPs that trigger multiple security rules
async function recordSecurityViolation(ip, reason) {
const key = `violations:${ip}:${getCurrentDay()}`;
const count = await redis.incr(key);
await redis.expire(key, 86400);
// Log the violation
await db.collection('security_violations').insertOne({
ip,
reason,
timestamp: new Date(),
count
});
// Block after 10 violations in one day
if (count >= 10) {
await redis.sAdd('blocked:ips', ip);
// Alert security team
await sendSecurityAlert('system', {
type: 'ip_auto_blocked',
ip,
violationCount: count
});
}
return count;
}
// Use in various security checks
app.use(async (req, res, next) => {
if (req.behaviorAnalysis?.isSuspicious) {
await recordSecurityViolation(req.ip, 'Suspicious behavior');
}
if (req.suspicionReasons?.length > 0) {
await recordSecurityViolation(req.ip, req.suspicionReasons.join(', '));
}
next();
});Monitoring and Alerts: Know When You're Under Attack
Security without monitoring is like having a burglar alarm with no bell. You need to know when attacks happen.
Real-Time Security Dashboard Metrics
// Collect security metrics every minute
async function collectSecurityMetrics() {
const now = Date.now();
const oneHourAgo = now - 3600000;
// Get all metrics in parallel
const [
totalRequests,
blockedRequests,
suspiciousRequests,
activeViolations,
blockedIPs,
hourlyCost
] = await Promise.all([
redis.get(`requests:${getCurrentHour()}`),
redis.get(`blocked:${getCurrentHour()}`),
redis.get(`suspicious:${getCurrentHour()}`),
db.collection('security_violations').countDocuments({
timestamp: { $gte: new Date(oneHourAgo) }
}),
redis.sCard('blocked:ips'),
redis.get(`global:cost:${getCurrentHour()}`)
]);
return {
timestamp: new Date(),
requests: {
total: parseInt(totalRequests || '0'),
blocked: parseInt(blockedRequests || '0'),
suspicious: parseInt(suspiciousRequests || '0')
},
security: {
violations: activeViolations,
blockedIPs: blockedIPs
},
costs: {
hourly: parseFloat(hourlyCost || '0')
}
};
}
// API endpoint for dashboard
app.get('/api/security/dashboard', async (req, res) => {
const metrics = await collectSecurityMetrics();
res.json(metrics);
});
// Store metrics for historical analysis
setInterval(async () => {
const metrics = await collectSecurityMetrics();
await db.collection('security_metrics').insertOne(metrics);
}, 60000); // Every minuteSmart Alerting: Don't Cry Wolf
Send alerts only when something is actually wrong.
// Alert thresholds
const ALERT_THRESHOLDS = {
requestsPerMinute: 1000, // Spike in traffic
blockRatePercent: 10, // >10% requests blocked
costPerHour: 500, // >$500/hour
violationsPerHour: 100, // >100 violations
newBlockedIPs: 50 // >50 IPs blocked in hour
};
async function checkAlertConditions() {
const metrics = await collectSecurityMetrics();
// Check 1: Traffic spike
if (metrics.requests.total > ALERT_THRESHOLDS.requestsPerMinute) {
await sendAlert({
severity: 'warning',
title: 'Traffic spike detected',
message: `${metrics.requests.total} requests in last minute`,
metric: 'traffic'
});
}
// Check 2: High block rate
const blockRate = (metrics.requests.blocked / metrics.requests.total) * 100;
if (blockRate > ALERT_THRESHOLDS.blockRatePercent) {
await sendAlert({
severity: 'critical',
title: 'High block rate',
message: `${blockRate.toFixed(1)}% of requests blocked`,
metric: 'security'
});
}
// Check 3: Cost spike
if (metrics.costs.hourly > ALERT_THRESHOLDS.costPerHour) {
await sendAlert({
severity: 'critical',
title: 'Cost spike detected',
message: `$${metrics.costs.hourly.toFixed(2)} spent in last hour`,
metric: 'cost'
});
}
// Check 4: Many violations
if (metrics.security.violations > ALERT_THRESHOLDS.violationsPerHour) {
await sendAlert({
severity: 'warning',
title: 'High security violations',
message: `${metrics.security.violations} violations in last hour`,
metric: 'security'
});
}
}
// Check every 5 minutes
setInterval(checkAlertConditions, 300000);
async function sendAlert(alert) {
// Prevent alert spam (max 1 alert per metric per hour)
const alertKey = `alert:sent:${alert.metric}:${getCurrentHour()}`;
const alreadySent = await redis.exists(alertKey);
if (alreadySent) return;
// Send to Slack
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `${alert.severity === 'critical' ? '🚨' : '⚠️'} ${alert.title}`,
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${alert.title}*\n${alert.message}`
}
}]
})
});
// Mark as sent
await redis.set(alertKey, '1', { EX: 3600 });
}Testing Your Defenses: Simulate Attacks
Don't wait for real attacks to find weak spots. Test your security regularly.
Simple Attack Simulation Script
// attack-simulator.js
// Run this to test your defenses (NOT on production!)
async function simulateAttack(type) {
const BASE_URL = 'http://localhost:3000';
switch (type) {
case 'rate-limit':
// Test: Rapid requests from single IP
console.log('Testing rate limiting...');
for (let i = 0; i < 200; i++) {
const res = await fetch(`${BASE_URL}/api/data`);
console.log(`Request ${i + 1}: ${res.status}`);
if (res.status === 429) {
console.log('✅ Rate limit working - blocked at request', i + 1);
break;
}
}
break;
case 'credential-stuffing':
// Test: Multiple login attempts
console.log('Testing login protection...');
const emails = ['test1@example.com', 'test2@example.com', 'test3@example.com'];
for (let i = 0; i < 50; i++) {
const email = emails[i % emails.length];
const res = await fetch(`${BASE_URL}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password: 'wrong' + i })
});
console.log(`Login attempt ${i + 1} (${email}): ${res.status}`);
}
break;
case 'sequential-scraping':
// Test: Sequential page access
console.log('Testing sequential access detection...');
for (let i = 1; i <= 50; i++) {
const res = await fetch(`${BASE_URL}/api/products/${i}`);
console.log(`Product ${i}: ${res.status}`);
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay
}
break;
case 'cost-attack':
// Test: Expensive endpoint abuse
console.log('Testing cost protection...');
for (let i = 0; i < 20; i++) {
const res = await fetch(`${BASE_URL}/api/ai/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token'
},
body: JSON.stringify({ prompt: 'Test prompt ' + i })
});
console.log(`AI request ${i + 1}: ${res.status}`);
if (res.status === 429) {
console.log('✅ Cost protection working');
break;
}
}
break;
}
}
// Run simulation
const attackType = process.argv[2] || 'rate-limit';
simulateAttack(attackType).catch(console.error);Run with: node attack-simulator.js credential-stuffing
Production Checklist: Deploy with Confidence
Before going live, verify these security measures:
Essential:
- •Rate limiting on all public endpoints
- •Stricter limits on login, registration, password reset
- •Cost tracking on expensive operations (AI, SMS, email)
- •IP reputation checking
- •Basic behavior analysis (timing, patterns)
- •Security violation logging
- •Cost alerts configured
Recommended:
- •Device fingerprinting
- •Email domain validation
- •Automatic IP blocking
- •Suspicious behavior challenges
- •Security dashboard monitoring
- •Slack/email alerts for incidents
Advanced:
- •Machine learning anomaly detection
- •Threat intelligence integration
- •Progressive challenge system
- •Detailed metrics and analytics
- •Regular penetration testing
Common Mistakes to Avoid
1. Rate limiting by IP only
- •Problem: Attackers use proxy networks with millions of IPs
- •Fix: Also rate limit by user ID, session, device fingerprint
2. Same security for all endpoints
- •Problem: Public endpoints need different protection than authenticated ones
- •Fix: Apply stricter limits to expensive/sensitive operations
3. Blocking too aggressively
- •Problem: False positives frustrate legitimate users
- •Fix: Use progressive challenges instead of immediate blocks
4. No monitoring
- •Problem: You don't know you're under attack until it's too late
- •Fix: Set up real-time monitoring and alerts
5. Storing everything in memory
- •Problem: Data lost on server restart
- •Fix: Use Redis or another persistent store
6. Revealing too much in errors
- •Problem: "Email not found" tells attackers which emails exist
- •Fix: Use generic messages like "Invalid credentials"
7. Trusting client-side validation
- •Problem: Attackers bypass frontend completely
- •Fix: Always validate on backend
Tools and Resources
Rate Limiting:
- •Redis: https://redis.io/
- •ioredis (Node.js client): https://github.com/luin/ioredis
IP Reputation:
- •IPQualityScore: https://www.ipqualityscore.com/
- •AbuseIPDB: https://www.abuseipdb.com/
- •Project Honeypot: https://www.projecthoneypot.org/
Email Validation:
- •Kickbox: https://kickbox.com/
- •Hunter.io: https://hunter.io/email-verifier
- •NeverBounce: https://neverbounce.com/
Bot Detection:
- •Cloudflare Bot Management: https://www.cloudflare.com/products/bot-management/
- •DataDome: https://datadome.co/
- •hCaptcha: https://www.hcaptcha.com/
Monitoring:
- •Grafana: https://grafana.com/
- •Datadog: https://www.datadoghq.com/
- •Sentry: https://sentry.io/
Security Testing:
- •OWASP ZAP: https://www.zaproxy.org/
- •Burp Suite: https://portswigger.net/burp
Conclusion
Security in 2026 isn't optional—it's survival. AI-powered attacks are faster, smarter, and more sophisticated than ever. But with the right defenses, you can protect your backend without sacrificing user experience or breaking the bank.
Start with the basics: implement solid rate limiting, track user behavior, and protect expensive operations. Then layer on advanced defenses like IP reputation checks, behavioral analysis, and cost monitoring.
Remember: security is a continuous process, not a one-time setup. Monitor your systems daily, review security logs weekly, and update your defenses as attacks evolve.
The attacks are already happening. The question is: are you ready to defend against them?