Skip to content

Hono Middleware

pg-ratelimit ships a pg-ratelimit/hono subpath export with a ready-made Hono middleware. It accepts a Ratelimit instance directly - no config duplication.

pnpm add pg-ratelimit pg hono
import { Hono } from "hono";
import { Pool } from "pg";
import { Ratelimit } from "pg-ratelimit";
import { ratelimit } from "pg-ratelimit/hono";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const limiter = new Ratelimit({
pool,
limiter: Ratelimit.slidingWindow(10, "1m"),
prefix: "api",
});
const app = new Hono();
app.use("/api/*", ratelimit({ limiter }));
app.get("/api/hello", (c) => c.json({ message: "Hello!" }));
app.use(
ratelimit({
limiter: myRatelimitInstance,
key: (c) => c.req.header("x-api-key") ?? "anonymous",
rate: 5,
response: (c, result) => c.json({ error: "nope" }, 429),
}),
);
OptionTypeDefaultDescription
limiterRatelimit-Required. Your Ratelimit instance
key(c: Context) => string | Promise<string>x-real-ipx-forwarded-for first entry → "anonymous"Extracts the identifier from each request
ratenumber | (c: Context) => number | Promise<number>1Tokens consumed per request
response(c: Context, result: LimitResult) => Response | Promise<Response>JSON { error: "Too many requests" } 429Custom rejection response

The middleware sets standard rate limit headers on every response (both allowed and rejected):

HeaderValue
RateLimit-LimitMaximum tokens allowed
RateLimit-RemainingTokens remaining after this request
RateLimit-ResetUnix timestamp (seconds) when the window/bucket resets

Rejected responses additionally include a Retry-After header with the number of seconds until the limit resets.

The default key extractor uses x-real-ip, then the first entry of x-forwarded-for, then falls back to "anonymous". Override it to key by API key, user ID, or anything else:

app.use(
"/api/*",
ratelimit({
limiter,
key: (c) => c.req.header("x-api-key") ?? "anonymous",
}),
);

Use the rate option to consume multiple tokens for expensive endpoints:

// Static cost
app.use("/api/export/*", ratelimit({ limiter, rate: 10 }));
// Dynamic cost
app.use(
"/api/*",
ratelimit({
limiter,
rate: (c) => (c.req.path.startsWith("/api/export") ? 10 : 1),
}),
);

Errors from the limiter (e.g. database unreachable) propagate directly to Hono’s error handler. The middleware does not fail open or fail closed - use Hono’s app.onError to decide:

// Fail open
app.onError((err, c) => {
console.error(err);
return c.text("Internal Server Error", 500);
});

A full working example lives at apps/hono-example/ in the repo. It starts Postgres via Docker Compose and runs a Hono server with sliding window rate limiting on port 3001.

Terminal window
cd apps/hono-example
pnpm dev
curl -i http://localhost:3001/api/hello