# Hono

> Step-by-step Hono rate limiting tutorial with @unkey/ratelimit. Protect routes on Node.js, Bun, or Cloudflare Workers without running Redis.

## What you'll build

A Hono app with rate-limited endpoints. Users who exceed the limit get a 429 response.

**Time to complete:** ~5 minutes

## Prerequisites

- [Unkey account](https://app.unkey.com/auth/sign-up) (free)
- [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission
- Node.js 18+ or Bun

<Steps titleSize="h3">

<Step title="Create a Hono app">

<CodeGroup>

```bash npm
npm create hono@latest unkey-hono-ratelimit
cd unkey-hono-ratelimit
```

```bash pnpm
pnpm create hono@latest unkey-hono-ratelimit
cd unkey-hono-ratelimit
```

```bash bun
bun create hono@latest unkey-hono-ratelimit
cd unkey-hono-ratelimit
```

</CodeGroup>

Choose your preferred runtime.

</Step>

<Step title="Install the SDK">

<CodeGroup>

```bash npm
npm install @unkey/ratelimit
```

```bash pnpm
pnpm add @unkey/ratelimit
```

```bash bun
bun add @unkey/ratelimit
```

</CodeGroup>

</Step>

<Step title="Add your root key">

Create a `.env` file:

```bash .env
UNKEY_ROOT_KEY="unkey_..."
```

</Step>

<Step title="Add rate limiting">

Update `src/index.ts`:

```ts src/index.ts
import { Hono } from "hono";
import { Ratelimit } from "@unkey/ratelimit";

const app = new Hono();

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

// Public route
app.get("/", (c) => {
  return c.json({ message: "Welcome! Try /api/data" });
});

// Rate-limited route
app.get("/api/data", async (c) => {
  // 1. Identify the user
  const identifier =
    c.req.header("x-user-id") ?? c.req.header("x-forwarded-for") ?? "anonymous";

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

  // 3. Set headers
  c.header("X-RateLimit-Limit", "10");
  c.header("X-RateLimit-Remaining", remaining.toString());
  c.header("X-RateLimit-Reset", reset.toString());

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

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

export default app;
```

</Step>

<Step title="Run your app">

<CodeGroup>

```bash npm
npm run dev
```

```bash pnpm
pnpm dev
```

```bash bun
bun dev
```

</CodeGroup>

</Step>

<Step title="Test it">

```bash
# 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 succeed. Requests 11+ get:

```json
{ "error": "Too many requests. Try again later." }
```

</Step>

</Steps>

## What's in the response?

`limiter.limit()` returns:

| Field       | Type      | Description                                |
| ----------- | --------- | ------------------------------------------ |
| `success`   | `boolean` | `true` if allowed, `false` if rate limited |
| `remaining` | `number`  | Requests left in current window            |
| `reset`     | `number`  | Unix timestamp (ms) when window resets     |
| `limit`     | `number`  | The configured limit                       |

## Using as middleware

Create reusable middleware for cleaner code:

```ts src/middleware/ratelimit.ts
import { Context, Next } from "hono";
import { Ratelimit } from "@unkey/ratelimit";

const limiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "api",
  limit: 100,
  duration: "60s",
});

export async function rateLimit(c: Context, next: Next) {
  const identifier =
    c.req.header("x-user-id") ?? c.req.header("x-forwarded-for") ?? "anonymous";

  const { success, remaining, reset } = await limiter.limit(identifier);

  c.header("X-RateLimit-Remaining", remaining.toString());
  c.header("X-RateLimit-Reset", reset.toString());

  if (!success) {
    return c.json({ error: "Rate limit exceeded" }, 429);
  }

  await next();
}
```

Apply to routes:

```ts src/index.ts
import { Hono } from "hono";
import { rateLimit } from "./middleware/ratelimit";

const app = new Hono();

// Public
app.get("/", (c) => c.json({ message: "Welcome" }));

// Rate-limited routes
app.use("/api/*", rateLimit);

app.get("/api/data", (c) => {
  return c.json({ data: "protected content" });
});

app.get("/api/user", (c) => {
  return c.json({ user: "info" });
});

export default app;
```

## Different limits per route group

```ts
const apiLimiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "api",
  limit: 100,
  duration: "60s",
});

const authLimiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "auth",
  limit: 5,
  duration: "60s",
});

// Apply different limiters to different route groups
app.use("/api/*", createRateLimitMiddleware(apiLimiter));
app.use("/auth/*", createRateLimitMiddleware(authLimiter));
```

## Deploying to Cloudflare Workers

For Cloudflare Workers, access env through the context:

```ts
app.get("/api/data", async (c) => {
  const limiter = new Ratelimit({
    rootKey: c.env.UNKEY_ROOT_KEY, // Access from c.env
    namespace: "api",
    limit: 10,
    duration: "60s",
  });

  const { success } = await limiter.limit("user-id");
  // ...
});
```

Set your secret with wrangler:

```bash
npx wrangler secret put UNKEY_ROOT_KEY
```

## Next steps

<CardGroup cols={2}>
  <Card title="How it works" icon="bolt" href="/platform/ratelimiting/how-it-works">
    Understand the architecture
  </Card>
  <Card
    title="Per-user overrides"
    icon="sliders"
    href="/platform/ratelimiting/overrides"
  >
    Give specific users higher limits
  </Card>
  <Card
    title="SDK Reference"
    icon="book"
    href="/libraries/ts/ratelimit/ratelimit"
  >
    All configuration options
  </Card>
  <Card title="Add API key auth" icon="key" href="/quickstart/apis/hono">
    Combine with API key authentication
  </Card>
</CardGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Rate limit not applying?">
    - Verify `UNKEY_ROOT_KEY` is set correctly - For Workers: use
    `c.env.UNKEY_ROOT_KEY`, not `process.env` - Check your root key has
    `ratelimit.*.limit` permission
  </Accordion>

  <Accordion title="Environment not loading locally?">
    - For Node.js: Install `dotenv` and add `import 'dotenv/config'` - For Bun:
    `.env` loads automatically - Restart the dev server after changes
  </Accordion>
</AccordionGroup>
