> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiveku.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add Rate Limiting to Your API

> Protect your API routes from abuse, spam, and excessive usage

Public API routes without rate limits get scraped, brute-forced, and overwhelmed within days of launch. Login endpoints get credential-stuffing attacks. Signup forms get spam-bot flooded. SMS-sending routes get exploited for free message delivery. Rate limiting is the simplest defense.

<Info>
  Common endpoints to rate-limit: login, signup, password reset, public search, contact form, SMS sending, any expensive AI or external-API call.
</Info>

## Three Paths

<Tabs>
  <Tab title="In-memory counter">
    Fastest to write, zero dependencies. Only works per-container — state is lost on cold start or if you have multiple instances. Fine for quick protection on low-traffic sites; not sufficient at scale.
  </Tab>

  <Tab title="Database-backed">
    Slower than memory (one DB roundtrip per request) but persistent and consistent across instances. Good if you already have a DB and don't want another service. Add an index on the rate-limit table's key column.
  </Tab>

  <Tab title="Redis / Upstash (recommended)">
    Fast AND distributed. Upstash offers a generous free tier and a REST API that works from any serverless environment. This is what most production sites should use.
  </Tab>
</Tabs>

## Redis Pattern with Upstash

<Steps>
  <Step title="Sign up at Upstash">
    Go to [upstash.com](https://upstash.com), create a Redis database. The free tier handles 10k commands/day — plenty for moderate-traffic sites.
  </Step>

  <Step title="Copy REST credentials">
    From the database page, copy:

    * `UPSTASH_REDIS_REST_URL`
    * `UPSTASH_REDIS_REST_TOKEN`
  </Step>

  <Step title="Add env vars in Hiveku">
    Settings > Environment Variables. Add both. See [Environment Variables](/how-tos/env-vars).
  </Step>

  <Step title="Install the SDK">
    ```bash theme={null}
    npm install @upstash/ratelimit @upstash/redis
    ```
  </Step>

  <Step title="Wire up an API route">
    ```typescript theme={null}
    import { Ratelimit } from '@upstash/ratelimit';
    import { Redis } from '@upstash/redis';

    const ratelimit = new Ratelimit({
      redis: Redis.fromEnv(),
      limiter: Ratelimit.slidingWindow(10, '60 s'),
    });

    export default async function handler(req: Request) {
      const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
      const { success, remaining, reset } = await ratelimit.limit(ip);

      if (!success) {
        return Response.json(
          { error: 'Too many requests' },
          {
            status: 429,
            headers: { 'Retry-After': String(reset) },
          }
        );
      }

      // Your actual handler logic
      return Response.json({ ok: true });
    }
    ```
  </Step>
</Steps>

## Common Limits by Endpoint

| Endpoint                | Suggested limit              |
| ----------------------- | ---------------------------- |
| Login                   | 5 attempts per minute per IP |
| Signup                  | 3 per minute per IP          |
| Password reset          | 3 per hour per email         |
| Public search           | 30 per minute per IP         |
| Contact form            | 5 per hour per IP            |
| Send SMS                | 10 per day per phone number  |
| AI chat / expensive API | 100 per day per user         |

These are starting points — tune based on your actual traffic patterns.

## Choosing the Rate-Limit Key

What you limit by matters as much as the number:

* **By IP.** Simple. Blocks coffee shops and corporate NATs if abused (false positives). Good for pre-auth endpoints where you have no better identifier.
* **By user ID.** Targeted. Doesn't help for pre-auth endpoints like login or signup.
* **By email or phone number.** Good for signup/login/reset — stops account-creation farming where an attacker rotates IPs but reuses the same email pattern.
* **Composite keys.** For login: limit by IP AND by email. An attacker hitting one email from many IPs triggers the email limit; one IP hitting many emails triggers the IP limit. Both must be low to get through.

## Return Proper 429 Response Headers

Clients can behave correctly if you tell them the limit state:

```typescript theme={null}
return Response.json(
  { error: 'Too many requests' },
  {
    status: 429,
    headers: {
      'Retry-After': String(reset),
      'X-RateLimit-Limit': '10',
      'X-RateLimit-Remaining': String(remaining),
      'X-RateLimit-Reset': String(reset),
    },
  }
);
```

`Retry-After` is the most important — well-behaved clients will back off for that many seconds.

## Client-Side Handling

When you get a 429:

<Steps>
  <Step title="Show a friendly error">
    Not "Error 429". Something like "You're doing that too fast. Try again in 30 seconds."
  </Step>

  <Step title="Show a countdown">
    Display a countdown timer based on `Retry-After`. Disable the submit button until it elapses.
  </Step>

  <Step title="Exponential backoff on retries">
    If your client auto-retries, double the delay each time: 1s, 2s, 4s, 8s. Cap at a sensible max.
  </Step>
</Steps>

## Database-Backed Pattern

If Redis isn't an option, a minimal DB approach:

```sql theme={null}
CREATE TABLE rate_limits (
  key text NOT NULL,
  window_start timestamptz NOT NULL,
  count integer DEFAULT 1,
  PRIMARY KEY (key, window_start)
);

CREATE INDEX ON rate_limits (window_start);
```

On each request:

```typescript theme={null}
const window = new Date(Math.floor(Date.now() / 60000) * 60000);
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';

const result = await db.query(
  `INSERT INTO rate_limits (key, window_start, count)
   VALUES ($1, $2, 1)
   ON CONFLICT (key, window_start)
   DO UPDATE SET count = rate_limits.count + 1
   RETURNING count`,
  [ip, window]
);

if (result[0].count > 10) {
  return new Response('Too many requests', { status: 429 });
}
```

Clean up old rows periodically with a scheduled workflow: `DELETE FROM rate_limits WHERE window_start < now() - interval '1 hour'`.

## What NOT to Rate-Limit

<Warning>
  Don't rate-limit endpoints a legitimate single user needs to hit often — autocomplete suggestions, incremental search, drag-and-drop save calls. Rate limits should prevent abuse, not break UX.
</Warning>

Good candidates: per-IP + per-user composite limits that are high enough never to hit in normal usage but low enough to block scripts.

## Verify It Worked

<Steps>
  <Step title="Hit the endpoint in a loop">
    ```bash theme={null}
    for i in {1..15}; do
      curl -i https://yoursite.com/api/login \
        -H "Content-Type: application/json" \
        -d '{"email":"test@test.com","password":"wrong"}'
      echo ""
    done
    ```
  </Step>

  <Step title="Confirm 429 after the limit">
    The first N requests should succeed (or return the usual login error). Request N+1 should return `429 Too Many Requests`.
  </Step>

  <Step title="Wait and retry">
    Wait for the `Retry-After` duration. The endpoint should accept requests again.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Limits aren't enforcing at all">
    Check env vars. If `UPSTASH_REDIS_REST_URL` or `UPSTASH_REDIS_REST_TOKEN` is missing, the SDK silently fails closed or open depending on config. Log the result of `ratelimit.limit(...)` to confirm it's actually being hit.
  </Accordion>

  <Accordion title="Legitimate users getting blocked">
    Your limits are too aggressive. Check your analytics for typical request rates from real users and set limits comfortably above the 99th percentile. Also consider switching from by-IP to by-user ID for authenticated endpoints.
  </Accordion>

  <Accordion title="Rate limit state lost on deploy">
    You're using in-memory limits. They don't survive cold starts or new deployments. Switch to Redis or DB-backed limits.
  </Accordion>

  <Accordion title="Behind a load balancer, everyone shares an IP">
    `x-forwarded-for` may contain a chain of IPs. Use the first one (original client), not the last. Also check your reverse proxy/CDN is adding this header correctly and not stripping it.
  </Accordion>

  <Accordion title="CloudFront/Cloudflare also blocking">
    Your WAF and app-level rate limits are double-counting. Either disable WAF rate limiting for these routes and handle it all at the app, or the opposite. Don't stack — it confuses users and makes debugging hard.
  </Accordion>
</AccordionGroup>

## What's Next?

<CardGroup cols={2}>
  <Card title="Environment Variables" icon="key" href="/how-tos/env-vars">
    Manage Redis and other service credentials safely
  </Card>

  <Card title="View Logs" icon="file-lines" href="/how-tos/view-logs">
    Debug rate limiting issues in production
  </Card>
</CardGroup>
