DEV Community

Cover image for The $274/5min Bot Attack: Protecting Next.js with Docker & Redis
Ameer Hamza
Ameer Hamza

Posted on

The $274/5min Bot Attack: Protecting Next.js with Docker & Redis

The Nightmare Scenario: $274 in 5 Minutes

Imagine waking up to a notification from your hosting provider. Not a "New User" alert, but a billing alert. In just five minutes, a malicious bot swarm hit your Next.js application, triggering a massive spike in serverless function execution and bandwidth. The cost? $274.

This isn't a hypothetical. It recently happened to a developer on Vercel Pro, and the fallout highlighted a critical vulnerability in modern "hands-off" hosting: when you scale automatically, your bill scales automatically too—even if the traffic is malicious.

As a full-stack developer who has built and shipped over 50 production systems, I've seen this pattern repeat. The "magic" of serverless is great until the bill arrives. Today, we're going to look at how to take back control by moving to a self-hosted Docker architecture with Redis-backed rate limiting.

Why Default Serverless Protection Isn't Enough

Most PaaS providers offer basic DDoS protection, but "Layer 7" attacks—bots that look like real users hitting expensive API routes—often slip through. If your /api/generate-pdf or /api/ai-query route isn't strictly rate-limited at the infrastructure level, you are writing a blank check to anyone with a script.

To truly protect your application (and your wallet), you need:

  1. Infrastructure Control: Moving to Docker allows you to set hard limits on resources.
  2. Stateful Rate Limiting: Using Redis to track requests across multiple instances.
  3. Edge-Level Filtering: Blocking known bad actors before they even hit your application logic.

Step 1: Dockerizing Next.js for Predictable Scaling

The first step in stopping the "infinite bill" is moving to a predictable resource model. With Docker, you pay for the server (EC2, DigitalOcean, etc.), not the individual request.

Here is a production-ready Dockerfile for a Next.js application:

# syntax=docker/dockerfile:1

FROM node:18-alpine AS base

# 1. Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
  else echo "Lockfile not found." && exit 1; \
  fi

# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Why this works: By using the standalone output mode in Next.js, we create a minimal production build that doesn't include node_modules, significantly reducing the attack surface and image size.


Step 2: Implementing Redis-Backed Rate Limiting

To prevent a bot from hammering your API, we need a way to track requests. A simple in-memory map won't work if you have multiple Docker containers. We need Redis.

We'll use upstash-ratelimit (or a local Redis instance with ioredis) to implement a sliding window algorithm.

// lib/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// Create a new ratelimiter, that allows 10 requests per 10 seconds
export const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  analytics: true,
  prefix: "@upstash/ratelimit",
});
Enter fullscreen mode Exit fullscreen mode

Step 3: The Next.js Middleware Shield

Now, we apply this protection globally using Next.js Middleware. This ensures that malicious requests are rejected before they trigger any expensive server-side logic or database queries.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { ratelimit } from './lib/ratelimit';

export async function middleware(request: NextRequest) {
  // Only protect API routes and sensitive pages
  if (request.nextUrl.pathname.startsWith('/api')) {
    const ip = request.ip ?? '127.0.0.1';
    const { success, limit, reset, remaining } = await ratelimit.limit(ip);

    if (!success) {
      return new NextResponse('Too Many Requests', {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        },
      });
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Advanced Bot Detection with Fingerprinting

IP-based rate limiting is a good start, but sophisticated bots use rotating proxies. To catch them, we need to look at more than just the IP address. We can create a "fingerprint" using headers.

Click to see the advanced fingerprinting logic
function getFingerprint(request: NextRequest) {
  const userAgent = request.headers.get('user-agent') || 'unknown';
  const acceptLanguage = request.headers.get('accept-language') || 'unknown';
  const ip = request.ip ?? '127.0.0.1';

  // Combine factors to create a unique ID for the "client"
  return `${ip}-${userAgent.substring(0, 50)}-${acceptLanguage}`;
}
Enter fullscreen mode Exit fullscreen mode

By using this fingerprint in your ratelimit.limit(fingerprint) call, you can catch bots that change IPs but keep the same browser signature.


Common Pitfalls & Edge Cases

  1. False Positives: If you have a high-traffic office or a school sharing a single public IP, IP-based limiting might block legitimate users. Fix: Use authenticated user IDs for rate limiting whenever possible.
  2. Redis Latency: Every request now waits for a Redis check. Fix: Use a globally distributed Redis (like Upstash) or keep your Redis instance in the same VPC as your Docker containers.
  3. The "Retry-After" Header: Bots often ignore it, but legitimate clients (like your own frontend) should respect it to avoid further blocking.

Conclusion

The "serverless dream" can quickly turn into a billing nightmare if you don't have proper guards in place. By moving to a Docker-based architecture and implementing stateful rate limiting with Redis, you gain:

  • Cost Predictability: No more surprise $200 bills.
  • Granular Control: You decide exactly how many requests are "too many."
  • Improved Security: You're actively defending against Layer 7 attacks.

What's your approach to handling bot traffic? Have you hit similar billing edge cases with serverless providers? Drop your thoughts in the comments.


About the Author: Ameer Hamza is a Top-Rated Full-Stack Developer with 7+ years of experience building SaaS platforms, eCommerce solutions, and AI-powered applications. He specializes in Laravel, Vue.js, React, Next.js, and AI integrations — with 50+ projects shipped and a 100% job success rate. Check out his portfolio at ameer.pk to see his latest work, or reach out for your next development project.


Top comments (0)