Skip to main content

What you’ll build

A Bun HTTP server with rate limiting. Users who exceed the limit get a 429 response. Time to complete: ~3 minutes

Prerequisites

1

Create a Bun project

mkdir unkey-bun-ratelimit && cd unkey-bun-ratelimit
bun init -y
2

Install the SDK

bun add @unkey/ratelimit
3

Add your root key

Create a .env file:
.env
UNKEY_ROOT_KEY="unkey_..."
4

Create your server

Replace index.ts:
index.ts
import { Ratelimit } from "@unkey/ratelimit";

// Create limiter instance
const limiter = new Ratelimit({
  rootKey: Bun.env.UNKEY_ROOT_KEY!,
  namespace: "bun-api",
  limit: 10,       // 10 requests...
  duration: "60s", // ...per minute
});

const server = Bun.serve({
  async fetch(req) {
    const url = new URL(req.url);

    // Public route
    if (url.pathname === "/") {
      return Response.json({ message: "Welcome! Try /api/data" });
    }

    // Rate-limited route
    if (url.pathname === "/api/data") {
      // 1. Identify the user
      const identifier = req.headers.get("x-user-id") 
        ?? req.headers.get("x-forwarded-for")
        ?? "anonymous";

      // 2. Check the rate limit
      const { success, remaining, reset } = await limiter.limit(identifier);

      // 3. Set headers
      const headers = {
        "X-RateLimit-Limit": "10",
        "X-RateLimit-Remaining": remaining.toString(),
        "X-RateLimit-Reset": reset.toString(),
      };

      if (!success) {
        return Response.json(
          { error: "Too many requests. Try again later." },
          { status: 429, headers }
        );
      }

      // 4. Request allowed
      return Response.json(
        { message: "Here's your data!", remaining },
        { headers }
      );
    }

    return Response.json({ error: "Not found" }, { status: 404 });
  },
  port: 3000,
});

console.log(`Server running at http://localhost:${server.port}`);
5

Run your server

bun run index.ts
6

Test it

# Hit the endpoint 12 times
for i in {1..12}; do
  curl http://localhost:3000/api/data -H "x-user-id: test-user"
  echo ""
done
First 10 requests return data. Requests 11+ get:
{ "error": "Too many requests. Try again later." }

What’s in the response?

limiter.limit() returns:
FieldTypeDescription
successbooleantrue if allowed, false if rate limited
remainingnumberRequests left in current window
resetnumberUnix timestamp (ms) when window resets
limitnumberThe configured limit

Multiple rate limiters

Create different limiters for different use cases:
index.ts
import { Ratelimit } from "@unkey/ratelimit";

// General API: 100/min
const apiLimiter = new Ratelimit({
  rootKey: Bun.env.UNKEY_ROOT_KEY!,
  namespace: "api",
  limit: 100,
  duration: "60s",
});

// Auth endpoints: 5/min (prevent brute force)
const authLimiter = new Ratelimit({
  rootKey: Bun.env.UNKEY_ROOT_KEY!,
  namespace: "auth",
  limit: 5,
  duration: "60s",
});

// Helper to apply rate limiting
async function checkLimit(limiter: Ratelimit, identifier: string) {
  const result = await limiter.limit(identifier);
  if (!result.success) {
    return Response.json(
      { error: "Rate limit exceeded" },
      { status: 429 }
    );
  }
  return null; // Allowed
}

const server = Bun.serve({
  async fetch(req) {
    const url = new URL(req.url);
    const ip = req.headers.get("x-forwarded-for") ?? "unknown";

    if (url.pathname === "/api/login") {
      const blocked = await checkLimit(authLimiter, ip);
      if (blocked) return blocked;
      
      return Response.json({ message: "Login endpoint" });
    }

    if (url.pathname.startsWith("/api/")) {
      const blocked = await checkLimit(apiLimiter, ip);
      if (blocked) return blocked;
      
      return Response.json({ message: "API data" });
    }

    return Response.json({ message: "Welcome" });
  },
  port: 3000,
});

Next steps

Troubleshooting

  • Verify UNKEY_ROOT_KEY is in your .env file
  • Use Bun.env.UNKEY_ROOT_KEY (not process.env)
  • Check your root key has ratelimit.*.limit permission
Bun loads .env automatically. Make sure the file is in your project root and restart the server.
Last modified on February 16, 2026