Learn how to protect public web APIs from DDoS spikes and scraper abuse using edge workers, Upstash Redis sliding windows, and Next.js.
Most developers implement rate limiting using an in-memory store like Express rate-limit or an internal Redis container running alongside their main backend. While this works for single-server hobby projects, it fails when traffic surges or when targeted by scrapers.
If a botnet floods your Next.js API routes with thousands of requests per second, each request still spins up a serverless function, consumes CPU time, and checks your database. By the time your backend responds with HTTP 429 Too Many Requests, your infrastructure bill has already climbed and legitimate users face high latency.
I encountered this exact problem while building [Aura Estate](/projects/15), a real estate portal with real-time property search and valuation estimates. Complex Supabase PostgreSQL queries for geospatial filters were vulnerable to aggressive scraping. Shifting rate checks to the network edge stopped unauthorized automated scrapers before they ever reached the database.
To stop DDoS attacks and brute-force traffic, your rate limiter needs to run on edge runtimes (such as Vercel Edge Middleware or Cloudflare Workers) and communicate with a low-latency database over HTTP REST rather than stateful TCP connections.
Upstash Redis works well here because it exposes an HTTP-based REST API that runs cleanly in lightweight edge environments without connection pool exhaustion. Here is a production-ready Next.js middleware implementation using a sliding window counter:
import { NextRequest, NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
// Initialize Upstash Redis with edge-friendly REST client
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Create a sliding window limiter: 20 requests per 10 seconds
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(20, '10 s'),
analytics: true,
prefix: '@edge-limiter',
});
export async function middleware(request: NextRequest) {
// Target only public API routes
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
// Extract client IP address safely
const ip = request.ip ?? request.headers.get('x-forwarded-for')?.split(',')[0] ?? '127.0.0.1';
// Limit based on client IP
const { success, limit, remaining, reset } = await ratelimit.limit(ip.trim());
if (!success) {
return new NextResponse(
JSON.stringify({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please wait before retrying.',
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
},
}
);
}
const response = NextResponse.next();
response.headers.set('X-RateLimit-Limit', limit.toString());
response.headers.set('X-RateLimit-Remaining', remaining.toString());
return response;
}
export const config = {
matcher: '/api/:path*',
};
IP-based throttling alone cannot stop distributed attacks because modern botnets cycle through thousands of residential proxy IP addresses. A reliable defensive strategy requires layered checks at the edge network:
Rate limiting is only half the battle. If your API returns data that changes infrequently, pairing your rate limiter with an Edge Cache Shield via the Cache-Control s-maxage directive cuts database load down to almost zero.
When building high-traffic endpoints, setting 'Cache-Control: public, s-maxage=60, stale-while-revalidate=300' lets the edge network serve cached responses directly from edge memory. The request never touches your rate-limiting Redis store or your database, absorbing volumetric read attacks automatically.
Securing web applications requires practical engineering choices that prevent downtime without degrading user experience. Explore my [Full-Stack Web Development Services](/services) to see how I build secure, high-speed applications, or [get in touch with Aafaque](/contact) directly to discuss your project security requirements.
When rate limiting runs on your primary server or database, abusive traffic still consumes compute cycles, memory, and database connections. Placing rate limiting on edge workers blocks malicious traffic at the CDN point of presence, preventing origin load and saving serverless execution costs.
The sliding window counter algorithm is ideal. Unlike a fixed window counter, which allows twice the limit of requests across window boundaries, sliding window counters smooth out traffic spikes and stop boundary-burst exploits.
Use a compound key. Combine the client IP (from CF-Connecting-IP or X-Forwarded-For) with user agent hashes, API tokens, or session IDs. For anonymous public routes, prioritize IP plus browser fingerprinting headers.