Loading...
Rate-Limiting
Author Cloudapp
E.G.

Next.js 14 - Rate Limiting with Upstash Redis made easy.

July 1, 2024
Table of Contents

Ensuring system availability is crucial for any product. However, misuse can overwhelm resources, or you may need to control usage and implement charges. Implementing rate limiting effectively addresses these issues and can be integrated seamlessly. Since your system is already in place, you want a solution that works well in stateless environments like serverless functions without additional design time.

Here is the GitHub repo with the full code, where you can see the Rate Limiting Integration, built with Upstash Redis.

Example page hosted on Vercel -> https://nextjs14-azureb2c-prisma.vercel.app/

Used Stack

I will start with my default stack:

  • Next.js 14 as the web framework, and I will use the provided API routes

  • Upstash Redis, in combination with the NPM Packages @upstash/ratelimit and @upstash/redis

  • Vercel for hosting

Two new NPM Packages

@upstash/ratelimit - Implements three different standardized algorithms so far, and you can read more about them in the project's README.

@upstash/redis - Is used to read/write to the DB

npm install @upstash/ratelimit @upstash/redis

Get Started with the Redis Database

Rate limiting with a single database is easy to implement. Start with the creation of a new Redis DB on Upstash:

Login and create Redis DB

Redis-Upstash
Redis-Upstash
Redis-Upstash-DB-Creation
Redis-Upstash-DB-Creation
Redis-Upstash-DB-Endpoint-password
Redis-Upstash-DB-Endpoint-password

Adding Env-Variables in .env.local

Use the two values copied before for the local .env file

# Redis Upstash
UPSTASH_REDIS_REST_URL="https://xxxxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN="xxxx"

Implement Rate Limiter in existing API Route

Let’s head over to an existing API Router. I will use the sitemap counter I created in a previous story.

import { NextRequest, NextResponse } from "next/server";
import { getSitemapEntries } from "@/lib/sitemapcounter";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// Create a new ratelimiter, that allows 1 requests per 45 seconds
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(1, "45 s"),
});

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const url = searchParams.get("url");

  // Use a constant string to limit all requests with a single ratelimit
  // Or use a userID, apiKey or ip address for individual limits.
  const identifier = "sitemapcounter";
  const { success } = await ratelimit.limit(identifier);

  if (!success) {
    console.log("Unable to process at this time");
    return NextResponse.json({ error: "Quota exceeded" }, { status: 429 });
  }

  if (!url) {
    return NextResponse.json({ error: "URL is required" }, { status: 400 });
  }

  const entries = await getSitemapEntries(url);

  if (entries !== null) {
    return NextResponse.json({ entries: entries || 0 });
  } else {
    return NextResponse.json(
      { error: "Failed to fetch or parse sitemap" },
      { status: 500 }
    );
  }

  // Alternatively, you could return a 202 Accepted status code

  return new NextResponse(null, { status: 202 });
}

Definition of the rate limiter

In this section, we define the rate limiter and say that only one call within 45 seconds is allowed.

// Create a new ratelimiter, that allows 1 requests per 45 seconds
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(1, "45 s"),
});

Rate Limit check

As a next step, we define an identifier (which could be the public IP address, a user ID (for logged-in users), etc.

// Use a constant string to limit all requests with a single ratelimit
  // Or use a userID, apiKey or ip address for individual limits.
  const identifier = "sitemapcounter";
  const { success } = await ratelimit.limit(identifier);

  if (!success) {
    console.log("Unable to process at this time");
    return NextResponse.json({ error: "Quota exceeded" }, { status: 429 });
  }

Error Message

If the check fails, we throw a “Quota exceeded” error.

Quota-exceeded
Quota-exceeded

Optimization needed for GlobalRatelimit

For projects on a bigger scale, you can define a “GlobalRateLimit” with nodes spread over the globe.

import { GlobalRatelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis";
// Create a new ratelimiter, that allows 10 requests per 10 seconds
const ratelimit = new GlobalRatelimit({
  redis: [
    new Redis({
      /* europe */
    }),
    new Redis({
      /* north america */
    }),
  ],
  limiter: Ratelimit.slidingWindow(10, "10 s"),
});

As you can see, the integration is straightforward. The same approach can be used for a billing model, where the customer pays for a certain number of API calls.

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles