Skip to content
Docs

Rate Limiting SDK

You can configure a custom rule with rate limit in your code by using the @vercel/firewall package. This can be useful in the following cases:

  • You need to set a rate limit on requests in your backend
  • You want to use additional conditions with the rate limit that are not possible in the custom rule configuration of the dashboard

Rate limit counters are tracked on a per-region basis; traffic matching a given rate limit key in multiple regions can exceed the limit you configure for any single region.

    1. From your dashboard, select the project that you'd like to configure rate limiting for. Then open Firewall in the sidebar
    2. Select Configure on the top right of the Firewall overview page. Then, select + New Rule
    3. Complete the fields for the rule as follows
      1. Type a name such as "Firewall api rule"
      2. In the Configure section, for the first If condition, select @vercel/firewall
      3. Use update-object as the Rate limit ID
      4. Use the default values for Rate Limit and Then
    4. Select Save Rule
    5. Apply the changes:
      • When you make any change, you will see a Review Changes button appear or update on the top right with the number of changes requested
      • Select Review Changes and review the changes to be applied
      • Select Publish to apply the changes to your production deployment
  1. You can now use the Rate limit ID update-object set up above with @vercel/firewall to rate limit any request based on your own conditions. Every request is counted against a rate limit key, and each unique key gets its own bucket. By default, the key is the client IP. Override it by passing a rateLimitKey (see Custom rate limit keys).

    rate-limit.ts
    import { checkRateLimit } from '@vercel/firewall';
     
    export async function POST(request: Request) {
      const { rateLimited } = await checkRateLimit('update-object', { request });
      if (rateLimited) {
        return new Response(
          JSON.stringify({
            error: 'Rate limit exceeded',
          }),
          {
            status: 429,
            headers: {
              'Content-Type': 'application/json',
            },
          },
        );
      }
      // Otherwise, continue with other tasks
    }
  2. For your code to run when deployed in a preview deployment, you need to:

Pass a rateLimitKey to bucket requests on something other than the client IP, such as an authenticated user ID or organization ID. The key you pass replaces the default client-IP bucket entirely: every request with the same key shares a bucket, even when the requests come from different IPs. To keep IP as part of the bucket, compose the key yourself (see below).

For example, this code will have a rate limit per authenticated user:

rate-limit.ts
import { checkRateLimit } from '@vercel/firewall';
import { authenticateUser } from './auth';
 
export async function POST(request: Request) {
  const auth = await authenticateUser(request);
  const { rateLimited } = await checkRateLimit('update-object', {
    request,
    rateLimitKey: auth.userId,
  });
  if (rateLimited) {
    return new Response(
      JSON.stringify({
        error: 'Rate limit exceeded',
      }),
      {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
        },
      },
    );
  }
  // Otherwise, continue with the request
}

Two or more values can be combined when you want to rate limit on multiple dimensions. Create a compound key from the separate values before passing it as rateLimitKey:

const auth = await authenticateUser(request);
const rateLimitKey = `${auth.orgId}:${auth.userId}`;
const { rateLimited } = await checkRateLimit('update-object', {
  request,
  rateLimitKey,
});

Pick a delimiter that does not appear in your id values, or use a structured encoding if parts are free-form, so two different pairs of values never produce the same string.

The following example shows how to use Firewall rule conditions in the dashboard together with a rateLimitKey you choose in code. The dashboard If conditions narrow which requests hit the rule (for example, a specific request header). Your function then calls checkRateLimit with a custom key.

Adding a dashboard condition to a rule does not restore per-IP bucketing. The bucket is defined by the rateLimitKey your function passes. If every matching request resolves to the same key (for example, a constant string), the rule becomes effectively global. Include the caller's IP or another per-caller value in the key when you want per-IP or per-user separation.

Edit the custom rule in the dashboard and add an If condition with the following values, then click Save Rule:

  • Filter dropdown: #Request Header
  • Value: xrr-internal-header
  • Operator: Equals
  • Match value: internal

In code, pass a rateLimitKey that matches how the buckets should be separated. This will only apply on request that matches the condition above.

rate-limit.ts
import { checkRateLimit } from '@vercel/firewall';
import { authenticateUser } from './auth';
 
export async function POST(request: Request) {
  const auth = await authenticateUser(request);
  const { rateLimited } = await checkRateLimit('update-object', {
    request,
    rateLimitKey: auth.orgId,
  });
  if (rateLimited) {
    return new Response(
      JSON.stringify({
        error: 'Rate limit exceeded',
      }),
      {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
        },
      },
    );
  }
}

Pass timeout to limit how long the rate-limit fetch can wait. This option applies to checkRateLimit and unstable_checkRateLimit in @vercel/firewall 1.2.6 and later:

OptionTypeDefaultDescription
timeoutnumberNo timeoutFetch timeout in milliseconds. The promise rejects with a TimeoutError when exceeded

The timeout limits the fetch duration, not the rate-limit counting window. For example, return a service-unavailable response if the check takes too long:

rate-limit.ts
import { checkRateLimit } from '@vercel/firewall';
 
export async function POST(request: Request) {
  try {
    const { rateLimited } = await checkRateLimit('update-object', {
      request,
      timeout: 1000,
    });
    if (rateLimited) {
      return new Response('Rate limit exceeded', { status: 429 });
    }
  } catch (error) {
    if (error instanceof Error && error.name === 'TimeoutError') {
      return new Response('Rate-limit check timed out', { status: 503 });
    }
    throw error;
  }
  return new Response('Rate-limit check passed', { status: 200 });
}
rate-limit.js
import { checkRateLimit } from '@vercel/firewall';
 
export async function POST(request) {
  try {
    const { rateLimited } = await checkRateLimit('update-object', {
      request,
      timeout: 1000,
    });
    if (rateLimited) {
      return new Response('Rate limit exceeded', { status: 429 });
    }
  } catch (error) {
    if (error instanceof Error && error.name === 'TimeoutError') {
      return new Response('Rate-limit check timed out', { status: 503 });
    }
    throw error;
  }
  return new Response('Rate-limit check passed', { status: 200 });
}
Last updated July 23, 2026

Was this helpful?

supported.