Lompat ke konten utama
vourdev
Kembali ke Blog

Tutorial

Stop API Abuse: Tutorial Rate Limiting Next.js App Router Pakai Upstash Redis

Panduan praktis mencegah bot spamming dan mengamankan endpoint Next.js App Router menggunakan algoritma Token Bucket dan Upstash Redis.

Gambar Cover Stop API Abuse: Tutorial Rate Limiting Next.js App Router Pakai Upstash Redis
Ditulis olehvourdev11 menit baca

Bayangkan skenario ini: kamu baru saja meluncurkan fitur AI wrapper atau payment gateway di aplikasi Next.js milikmu. Semuanya berjalan lancar sampai kamu terbangun di jam 3 pagi karena notifikasi billing cloud yang membengkak hingga puluhan juta rupiah. Penyebabnya? Seorang bot master menemukan endpoint API publik milikmu dan menembak ribuan request otomatis per menit tanpa proteksi.

Di arsitektur serverless modern seperti Vercel, AWS Lambda, atau Cloudflare Pages, masalah ini makin krusial. Pendekatan lama menggunakan in-memory counter (seperti modul express-rate-limit atau variabel Map global) tidak akan berfungsi. Artikel ini akan mengupas tuntas cara mengamankan Next.js App Router menggunakan Upstash Redis dan Token Bucket Algorithm yang tahan terhadap skalabilitas serverless.

Kenapa In-Memory Rate Limiting Gagal di Serverless

Pada aplikasi monolithic tradisional yang berjalan di satu server (seperti VPS Nginx + Node.js), variabel di dalam memori proses Node.js bertahan selama server menyala. Kamu bisa membuat objek const requestLog = {} untuk menghitung berapa kali IP tertentu melakukan panggilan.

Di arsitektur serverless Next.js, setiap request dapat diproses oleh isolated function instance (container) yang berbeda. Instance ini bisa mati (cold start) atau bertambah banyak secara otomatis (auto-scaling).

Perbandingan In-Memory State vs Centralized Redis di Serverless Environment MASALAH: In-Memory State Lambda Instance A Memory Count: 1 Lambda Instance B Memory Count: 1 State terisolasi di tiap instance. Client bisa bypass rate limit dengan menembak IP berbeda. SOLUSI: Centralized Redis State Instance A Instance B Upstash Redis Global State Sync
Perbandingan In-Memory State vs Centralized Redis di Serverless Environment

Jika client mengirimkan 10 request berturut-turut, Vercel dapat menyebarkannya ke 5 instance Lambda yang berbeda. Jika batas limit kamu adalah 5 request/menit, setiap instance Node.js hanya mencatat 2 request. Akibatnya, client berhasil meloloskan 10 request (seharusnya terblokir).

Oleh karena itu, kita membutuhkan centralized data store yang cepat, terdistribusi, dan menggunakan protokol HTTP/REST agar ramah dengan lingkungan serverless yang tidak mendukung TCP long-connection secara persisten. Di sinilah Upstash Redis masuk.


Algoritma Rate Limiting: Kenapa Token Bucket?

Ada beberapa algoritma rate limiting yang populer:

  1. Fixed Window: Menghitung request dalam interval waktu tetap (misal: 00:00 - 00:01). Kelemahannya: rentan terhadap burst traffic di batas pergantian menit.
  2. Sliding Window Log: Sangat akurat tetapi memakan memori Redis yang besar karena menyimpan setiap timestamp request.
  3. Token Bucket: Algoritma paling seimbang untuk aplikasi skala produksi.
Cara Kerja Algoritma Token Bucket pada Upstash Ratelimit Bucket Capacity Maksimal: 10 Token T T T Refill: +2 token / 10dtk Incoming Request Ada Token? 200 OK (Kurangi 1) 429 Too Many Req
Cara Kerja Algoritma Token Bucket pada Upstash Ratelimit

Prinsip Token Bucket:

  • Bayangkan sebuah ember (bucket) yang diisi token secara berkala dengan kecepatan tetap (misal: 10 token setiap 10 detik).
  • Setiap request masuk mengambil 1 token dari ember.
  • Jika token masih ada, request diizinkan lewat (200 OK).
  • Jika token habis, request ditolak langsung dengan status code 429 Too Many Requests.
  • Algoritma ini memungkinkan sedikit burst capacity (pengguna bisa menghabiskan sisa token dengan cepat) tanpa merusak infrastruktur backend.

Setup Project Next.js dan Client Upstash

Dashboard manajemen database Upstash Redis Serverless
Dashboard Upstash Redis untuk memantau request dan penggunaan database. · Luke Chesser (Unsplash)

Mari masuk ke tahap koding. Pertama, siapkan akun gratis di Upstash, buat database Redis baru, lalu ambil variabel lingkungan UPSTASH_REDIS_REST_URL dan UPSTASH_REDIS_REST_TOKEN.

Step 1: Install Package

Jalankan perintah berikut di terminal Next.js milikmu:

bash
npm install @upstash/ratelimit @upstash/redis

Step 2: Konfigurasi Environment Variables

Tambahkan kredensial Upstash ke file .env.local:

env
UPSTASH_REDIS_REST_URL="https://your-database-id.upstash.io"
UPSTASH_REDIS_REST_TOKEN="AXXXACMgFkZGVkY2FjOWI4..."
Keamanan: Pastikan .env.local tidak ikut masuk ke repositori Git milikmu. Gunakan Vercel Environment Variables saat melakukan deployment ke staging atau production.

Step 3: Membuat Instance Redis dan Ratelimit Client

Buat file baru di lib/ratelimit.ts. Kita akan menginisialisasi client Redis dan mengonfigurasi instance rate limiter yang siap dipakai di seluruh bagian aplikasi.

typescript
import { Redis } from "@upstash/redis";
import { Ratelimit } from "@upstash/ratelimit";

// Inisialisasi REST Redis Client
export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

/**
 * Global Rate Limiter:
 * Menggunakan algoritma Token Bucket
 * - Kapasitas maksimal: 10 request
 * - Refill rate: 10 request setiap 10 detik
 */
export const globalRatelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.tokenBucket(10, "10 s", 10),
  analytics: true,
  prefix: "@upstash/ratelimit/global",
});

/**
 * Strict Rate Limiter untuk Auth/Sensitive Endpoints:
 * - Fixed window: 5 request per 1 menit
 */
export const strictRatelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.fixedWindow(5, "60 s"),
  analytics: true,
  prefix: "@upstash/ratelimit/strict",
});

Penjelasan Kode lib/ratelimit.ts:

  1. new Redis({...}): Menggunakan koneksi berbasis HTTP/REST yang dioptimalkan untuk Edge & Serverless Runtimes (tanpa kelemahan TCP connection pooling).
  2. Ratelimit.tokenBucket(10, "10 s", 10): Parameter pertama adalah jumlah refill (10), parameter kedua adalah selang waktu refill ("10 s"), dan parameter ketiga adalah kapasitas maksimum bucket (10).
  3. analytics: true: Mengaktifkan pengumpulan statistik penggunaan rate limit secara latar belakang di Upstash Dashboard.
  4. prefix: Menambahkan namespace pada kunci Redis agar kunci antar fitur tidak saling bertabrakan (misalnya memisahkan kunci rate limit publik dan kunci login).

Implementasi Rate Limiting di Next.js Middleware

Struktur arsitektur Next.js Middleware dan Routing
Arsitektur Middleware Next.js memotong request sebelum mencapai Route Handler. · James Wiseman (Unsplash)

Cara paling efektif untuk mengamankan seluruh route aplikasi adalah dengan mencegat request di Next.js Middleware. Middleware berjalan di Edge Runtime sebelum request mencapai server-side render (SSR) atau API Route Handler.

Alur Eksekusi Next.js Middleware dengan Validasi Upstash Redis Client Next.js Middleware Extract Client IP Check Ratelimit HTTP REST (Upstash) Upstash Redis Status 429 Block NextResponse.next()
Alur Eksekusi Next.js Middleware dengan Validasi Upstash Redis

Buat file middleware.ts di direktori akar project kamu (/middleware.ts atau /src/middleware.ts):

typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { globalRatelimit } from "@/lib/ratelimit";

export async function middleware(request: NextRequest) {
  // 1. Ekstrak IP address milik user
  const ip = request.ip ?? request.headers.get("x-forwarded-for") ?? "127.0.0.1";

  // 2. Tembak Upstash Ratelimit check
  const { success, limit, reset, remaining } = await globalRatelimit.limit(
    `mw_${ip}`
  );

  // 3. Jika limit terlampaui, kembalikan HTTP 429 Too Many Requests
  if (!success) {
    return new NextResponse(
      JSON.stringify({
        error: "Too Many Requests",
        message: "Kamu telah melebihi batas request. Coba beberapa saat lagi.",
      }),
      {
        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(),
        },
      }
    );
  }

  // 4. Jika sukses, teruskan request dan sematkan header statistik
  const response = NextResponse.next();
  response.headers.set("X-RateLimit-Limit", limit.toString());
  response.headers.set("X-RateLimit-Remaining", remaining.toString());
  response.headers.set("X-RateLimit-Reset", reset.toString());

  return response;
}

// Konfigurasi matcher untuk menentukan route mana saja yang diproteksi
export const config = {
  matcher: [
    /*
     * Match semua request API:
     * - /api/:path*
     */
    "/api/:path*",
  ],
};

Penjelasan Kode middleware.ts:

  1. request.ip ?? request.headers.get("x-forwarded-for"): Mengambil alamat IP pengirim. Di platform seperti Vercel atau Cloudflare, IP tersimpan otomatis di header x-forwarded-for.
  2. await globalRatelimit.limit('mw_' + ip): Memeriksa dan memperbarui state token IP tersebut di Redis.
  3. Response Headers Standard (`X-RateLimit-*`): Kita wajib menyertakan header ini agar API client (seperti frontend React atau SDK pihak ketiga) mengetahui berapa sisa kuota (remaining) dan kapan kuota di-reset (reset).
  4. Retry-After: Memberi tahu browser/bot berapa detik mereka harus menunggu sebelum diperbolehkan mengirim request ulang.

Proteksi Granular pada Route Handler (Next.js App Router)

Kadang proteksi di level middleware belum cukup. Contoh: Route pencarian produk boleh diakses 100x per menit, tetapi endpoint Auth Login atau Generasi AI hanya boleh diakses 5x per menit per user ID.

Berikut contoh proteksi spesifik di Route Handler app/api/generate/route.ts:

typescript
import { NextRequest, NextResponse } from "next/server";
import { strictRatelimit } from "@/lib/ratelimit";

export async function POST(request: NextRequest) {
  try {
    // Simulasi mengekstrak identifier unik (misal: ID user terautentikasi atau IP)
    const userId = request.headers.get("x-user-id") ?? request.ip ?? "anonymous";

    // Panggil strict limit khusus endpoint AI
    const { success, limit, remaining, reset } = await strictRatelimit.limit(
      `ai_gen_${userId}`
    );

    if (!success) {
      const secondsLeft = Math.ceil((reset - Date.now()) / 1000);
      return NextResponse.json(
        {
          error: "Quota Exceeded",
          message: `Batas eksekusi AI tercapai. Silakan tunggu ${secondsLeft} detik lagi.`,
        },
        {
          status: 429,
          headers: {
            "X-RateLimit-Limit": limit.toString(),
            "X-RateLimit-Remaining": remaining.toString(),
            "X-RateLimit-Reset": reset.toString(),
          },
        }
      );
    }

    // Mengambil payload body dari client
    const body = await request.json();
    const { prompt } = body;

    if (!prompt) {
      return NextResponse.json(
        { error: "Bad Request", message: "Prompt wajib diisi." },
        { status: 400 }
      );
    }

    // Proses dummy pemanggilan model AI
    const aiResponse = `Hasil komputasi AI untuk prompt: "${prompt}"`;

    return NextResponse.json(
      { success: true, data: aiResponse },
      {
        status: 200,
        headers: {
          "X-RateLimit-Limit": limit.toString(),
          "X-RateLimit-Remaining": remaining.toString(),
        },
      }
    );
  } catch (error) {
    return NextResponse.json(
      { error: "Internal Server Error", message: (error as Error).message },
      { status: 500 }
    );
  }
}

Detail Penting Implementasi Route Handler:

  • Identifikasi Berbasis User ID: Dibanding hanya mengandalkan IP (yang bisa berubah jika user menggunakan VPN atau koneksi seluler), gunakan userId dari session JWT pengguna yang sudah login.
  • Isolasi Namespace: Penggunaan ai_gen_${userId} memastikan kuota endpoint ini independen dan tidak mengganggu kuota endpoint lain.

Gotchas & Best Practices di Production

Implementasi rate limiting sederhana bisa berdampak fatal jika kamu melupakan aspek-aspek edge-case berikut:

1. Penanganan IP Spoofing Header

Jika aplikasi Next.js kamu berada di balik proxy tambahan (misal: Cloudflare -> Vercel -> Next.js), header x-forwarded-for bisa dikirim secara palsu oleh attacker.

Peringatan: Selalu pastikan platform hosting milikmu merilis IP header yang valid. Pada Vercel, gunakan header bawaan request.ip yang dijamin aman dan telah disanitasi oleh Edge Network Vercel.

2. Fallback Mode Saat Upstash Redis Down (Graceful Degradation)

Meskipun SLA Upstash sangat tinggi (99.99%), jaringan publik tetap bisa mengalami gangguan (network timeout). Jangan biarkan outage Redis membuat seluruh aplikasi web kamu ikut runtuh (fail-closed).

Bungkus eksekusi limit dalam blok try...catch dan terapkan strategi Fail-Open:

typescript
let success = true;
try {
  const result = await globalRatelimit.limit(`mw_${ip}`);
  success = result.success;
} catch (error) {
  console.error("Upstash Redis connection timeout/failed:", error);
  // Fail-open: Izinkan request lewat demi menjaga ketersediaan layanan
  success = true; 
}

if (!success) {
  return new NextResponse("Too Many Requests", { status: 429 });
}

3. Mengoptimalkan Latensi (Ephemeral Cache)

@upstash/ratelimit memiliki fitur bawaan bernama ephemeralCache. Fitur ini menyimpan nilai limit di dalam memori lokal instance Node.js secara singkat untuk mengurangi jumlah panggilan HTTP REST ke Upstash Redis.

typescript
export const globalRatelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.tokenBucket(10, "10 s", 10),
  ephemeralCache: new Map(), // Memangkas latensi RTT Redis hingga 80%
});

Kesimpulan & Action Items

Rate limiting bukan lagi sekadar fitur tambahan, melainkan garda depan pertahanan aplikasi web modern dari kehancuran finansial dan DDoS attack. Kombinasi Next.js App Router dan Upstash Redis memberikan solusi rate limiting serverless yang efisien, berlatensi rendah, serta mudah dikembangkan.

Langkah Konkret Sekarang:

  1. Audit Endpoint: Identifikasi route API yang memakan resource paling mahal di aplikasi Next.js milikmu (endpoint AI, Auth, Payment, Send Email).
  2. Setup Upstash Redis: Buat database Redis Serverless gratis di Upstash dan simpan kredensial REST di environment variable.
  3. Pasang Middleware: Terapkan rate-limiting global pada file middleware.ts untuk memblokir bot spammer secara otomatis di Edge Layer.
  4. Uji Pengujian Beban: Manfaatkan tool seperti k6 atau autocannon untuk memverifikasi bahwa HTTP 429 dikembalikan dengan benar saat limit terlampaui.

Butuh penyesuaian khusus untuk project Anda?

Jika situasi operasional atau arsitektur sistem bisnis Anda membutuhkan solusi kustom, diskusikan langsung bersama tim engineer kami. Anda juga bisa melihat rincian layanan vour.dev atau menghitung estimasi biaya project lebih dulu.

Mulai Project