APIs

Protecting Your Next.js APIs with Upstash Rate Limiting

In web applications, security is a serious concern because it can cost you a lot of money. Every public API is an open door, and rate limiting is the guard that protects that door.

KUZUE
July 11, 2026
10 min read
5 views

Protecting Your Next.js APIs with Upstash Rate Limiting

In web applications, security is a serious concern because it can cost you a lot of money. Every public API is an open door, and rate limiting is the guard that protects that door.

It is also a good practice to log important events. A logger is a central place where an application records important events, making it easier to monitor, debug, and identify suspicious activities.

Let's look at some of the security concerns that make rate limiting necessary.

Cryptographic Exhaustion (DoS)

When a user logs into your application, the server verifies the user's hashed password using libraries like bcrypt.

Password hashing is intentionally CPU-intensive, meaning it requires significant processing power.

Now imagine a bot sends 10,000 login requests in a single second. Your server will attempt to verify every password hash. CPU usage will quickly spike to 100%, and your server will eventually freeze trying to process those hashes. As a result, legitimate users won't be able to log in.

This is known as cryptographic exhaustion, a form of Denial of Service (DoS) attack.

Brute Force Attacks

Now imagine a malicious script sends 10,000 different passwords to your login API. Before long, it may eventually guess the correct password.

However, if you protect your login endpoint by allowing only 5 login attempts per minute, it would take the attacker an extremely long time to guess the correct password, making the attack practically ineffective.

Email Abuse

Let's take another example.

Suppose your signup endpoint automatically sends a verification email whenever a user creates an account.

If a bot submits 10,000 signup requests, your application will trigger 10,000 verification emails.

If you're using an email provider like Resend, those emails could cost you millions of naira.

I have attempted to explain why we need to protect our API routes from attackers.

In this article, we'll be using @upstash/redis and @upstash/ratelimit to achieve this.


What is Redis?

Before we implement rate limiting, let's first understand the tools we'll be using. Redis is short for Remote Dictionary Server. Redis is a database, but it works differently from traditional relational databases. Instead of storing data in tables and rows, Redis stores data as key-value pairs.

One of the reasons Redis is so popular is because it is extremely fast. Since it stores data in memory, reading and writing data takes only a few milliseconds.

Redis also supports TTL (Time To Live), which allows keys to expire automatically after a specified period. This feature makes Redis an excellent choice for caching, session storage, and rate limiting.

@upstash/redis

@upstash/redis is the Redis client we'll use to communicate with our Redis database. Think of it as the bridge between our Next.js application and the Redis database hosted on Upstash.

@upstash/ratelimit

@upstash/ratelimit is a rate-limiting library designed to run efficiently on Edge runtimes. It uses Redis as its data store and provides highly efficient algorithms, such as the Sliding Window algorithm. Whenever a request comes into your application, the library communicates with Redis to determine how many requests that particular IP address has made within the configured time window. If the client is still within the allowed limit, the request is allowed. Otherwise, the request is rejected.


Getting Your Redis Credentials

Now let's create our Redis database.

Visit the Upstash Console: https://console.upstash.com

After creating an account:

  1. Click Create Database.
  2. Enter any database name.
  3. Select Redis as the database type.
  4. Choose your preferred region.
  5. Leave TLS enabled.
  6. Click Create.

Note

The free plan currently allows you to create one Redis database. Once your database has been created, copy the credentials into your .env file.

env
UPSTASH_REDIS_REST_URL="your_url"
UPSTASH_REDIS_REST_TOKEN="your_token"

Installing the Packages

npm install @upstash/redis @upstash/ratelimit


Creating the Redis Client

You can create a separate file for your Redis configuration and import it into your middleware.

ts
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

The Redis constructor accepts two important properties. The url tells the Redis client where your Redis database is located. The token is your secret credential that authenticates your application with Redis.

Together, these two properties establish secure communication between your Next.js application and your Redis database hosted on Upstash.


Creating the Rate Limiter

ts
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "1 m"),
  analytics: true,
});

Let's break down what each property does.

redis: This tells the rate limiter where to store and retrieve request history. Every time a client makes a request, the rate limiter communicates with Redis to update and read the request count.

limiter: Ratelimit.slidingWindow(10, "1 m") This is the mathematical algorithm responsible for deciding whether a request should be allowed.

The example above means:

Allow a maximum of 10 requests within a sliding window of one minute.

The library supports the following time units:

ms — milliseconds,s — seconds, m — minutes, h — hours, d — days

analytics: true,: This is a boolean that tells Upstash whether it should collect analytics for your rate limiter. When enabled, Upstash automatically records statistics such as:

Total requests, -Allowed requests, -Blocked requests These statistics are then visualized for you in the Upstash dashboard.


How .limit() Works

Now that we've configured our rate limiter, let's see how it works. The @upstash/ratelimit library provides us with a .limit() method.

ts
const { success, limit, remaining, reset } = await ratelimit.limit(ip);

The IP address is used as the rate limit key. Under the hood, the .limit(ip) method automatically communicates with Redis. It updates the request counter for that IP address, checks whether the client is still within the configured limit, and then returns the result.

If the client is within the configured limit: success === true;

If the client exceeds the configured limit: success === false; Your application can then return a 429 (Too Many Requests) response to the client.


Understanding the Response

The .limit() method returns four important properties.

ts
const { success, limit, remaining, reset } = await ratelimit.limit(ip);

Let's look at what each one means.

success: This indicates whether the request is allowed.

  • true means the client is still within the configured limit.
  • false means the client has exceeded the configured limit.

limit

This is the maximum number of requests you configured. For example, if you configured:

Ratelimit.slidingWindow(10, "1 m") then limit will always be 10.


remaining: This tells us how many requests the client still has before reaching the configured limit. For example, if the limit is 10 and the client has already made 3 requests, then: remaining = 7


reset:This is a timestamp indicating when the client can make requests again after reaching the configured limit.


Rate Limiting Algorithms

The Upstash rate limiter provides three well-tested algorithms. The algorithm you choose depends on how you want your application to behave.

The Fixed Window algorithm resets the request counter at fixed intervals.

For example:

Allow 10 requests between 12:00 and 12:01. Once the clock reaches 12:01, the counter resets back to zero. This algorithm is simple and fast, but it can sometimes allow traffic spikes around the boundary of each time window.


The Sliding Window algorithm doesn't wait for the clock to reset. Instead, the time window continuously moves with time. For example:

Allow 10 requests in any moving 60-second window. This makes traffic much smoother and is generally more secure than the Fixed Window algorithm. This is the algorithm we'll be using throughout this article.


The Token Bucket algorithm works a little differently. It allows clients to make a short burst of requests while gradually replenishing the available tokens over time.

Once all the tokens have been used, requests are throttled until more tokens become available. This algorithm works well for applications that occasionally need to handle bursts of traffic without allowing continuous abuse.


Rate Limit Headers

It is good practice to return rate limit information to the client. These headers allow clients to know how many requests they have remaining before reaching the configured limit.

Some commonly used headers include:

http
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset

Understanding process.env

Before we continue, let's briefly understand process.env.

In Node.js, process is a global object that provides information about the currently running program and its environment.

env is an object on process that contains your environment variables, such as API keys, database URLs, and secret tokens.

Whenever you write: process.env.UPSTASH_REDIS_REST_URL you're simply accessing an environment variable stored in your application.


Getting the Client's IP Address

The IP address comes from the network connection, not from the HTTP request body. You might think getting the client's IP address is as simple as writing: request.ip

However, this isn't always the case. In fact, NextRequest doesn't provide a request.ip property. When your application is deployed, your server usually doesn't communicate directly with the user's browser. Instead, it communicates with a proxy or load balancer.

For example, if you deploy your application on Vercel, your server doesn't receive requests directly from the browser—it receives them from Vercel.

To preserve the original client's IP address, proxies add special HTTP headers before forwarding the request.

The most common ones are:

  • x-forwarded-for
  • x-real-ip
  • cf-connecting-ip

For example:

http
x-forwarded-for: 10.44.45.44

Sometimes a request passes through multiple proxies. In that case, x-forwarded-for may contain multiple IP addresses separated by commas.

http
x-forwarded-for: 10.44.45.44, 172.16.10.5, 192.168.1.20

In most cases, the first IP address is the original client's IP address.

That's why, in production, it's a good practice to check these headers in the following order:

  1. x-forwarded-for
  2. x-real-ip
  3. cf-connecting-ip

During local development (npm run dev), there usually isn't a proxy, so these headers may not exist.

Note

Never blindly trust the value of x-forwarded-for, as clients can spoof this header in some environments.

Fortunately, platforms like Vercel validate these headers before forwarding requests to your application, making them safe to use in most deployments.


Logging Important Events

Earlier, I mentioned that logging is a good practice.

Whenever an important event occurs—such as a blocked request, a failed login attempt, or an unexpected error—it's helpful to record it. These logs make it much easier to monitor your application, investigate suspicious activities, and debug production issues. As your application grows, you'll likely use dedicated logging services instead of relying solely on the console.


console.log() vs console.error()

Both functions print messages to the console, but they are not exactly the same. console.log("User logged in successfully")

console.log() writes to stdout (standard output). It is commonly used for general application logs and informational messages.

console.error("Database connection failed");

console.error() writes to stderr (standard error).

This distinction becomes important in production because many logging systems separate normal application logs from error logs. This makes monitoring, debugging, and alerting much easier.


Conclusion

In this article, we looked at why rate limiting is an important security measure for every web application.

We discussed how it protects your APIs against attacks such as cryptographic exhaustion, brute-force attacks, and email abuse.

We then introduced Redis and the Upstash libraries we'll use to implement rate limiting, created a Redis client, configured a rate limiter, explored how .limit() works under the hood, and learned how to identify the client's IP address in production.

Rate limiting is one of the simplest security measures you can add to an application, yet it can prevent abuse, reduce infrastructure costs, and improve the overall reliability of your APIs.

In the next article, we'll put everything we've learned into practice by implementing rate limiting in a Next.js application.

Comments (0)

No comments yet. Be the first to share your thoughts.