Frameworks / Next.js
Next.js API key checks and rate limits
Wrap each Next.js route handler with UsageRail: it checks the API key, rate limit and quota before your handler runs, and counts the request once the response is ready. This page covers App Router route handlers on Vercel, or anywhere else Next.js runs.
npm install @usagerail/nodeCreate the instance once, at module level, so every route on an instance shares it:
import { after } from "next/server";
import { usageRail } from "@usagerail/node/next";
export const rail = usageRail({
appKey: process.env.USAGERAIL_APP_KEY,
auth: "api-key",
waitUntil: after,
});Then wrap each handler and give it its route template:
import { rail } from "@/lib/usagerail";
export const GET = rail.route("/v1/items/[id]", async (request, { params, usage }) => {
const { id } = await params;
const item = await loadItem(id);
usage.track(item.size); // optional: count units instead of requests
return Response.json(item);
});The key comes from the Authorization: Bearer header, or from x-api-key when the request has no bearer token.
Route templates and the runtime
- Name the template yourself. Next.js doesn't tell the handler which file it came from, so
rail.route()takes the template as its first argument. Write it the way the folder is named, such as/v1/items/[id], and use the same template for route weights in the console. With the raw path instead, every ID would become a route of its own. - Node.js runtime. Route handlers run on the Node.js runtime by default, and that's the one the SDK supports. It uses
node:crypto, which the Edge runtime doesn't have. - Paths without a handler. Next.js answers them with its own 404 before UsageRail sees them, so they aren't recorded.
- Vercel functions without Next.js. Use the same
usageRail()andrail.route(), and passwaitUntilfrom@vercel/functionsinstead ofafter.
How counts get sent
Between requests a serverless instance can be frozen, so there's no timer running in the background. Instead, after each response goes out, after() sends the counts, checks for changes such as revoked keys or new limits, and renews quota leases:
- One instance sends a batch at most every 0.5 seconds. Requests that end meanwhile go out with the next batch, and the last request always takes the remaining counts with it.
- A batch that fails stays in memory and goes out with the next request.
- An instance's first request waits up to 1 second for the key list. If that takes longer, keys are checked with UsageRail one by one until it arrives.
- If the key list is more than 20 seconds old, a request tries to refresh it first, waiting up to 1 second, so a revoked key normally stops working within about 20 seconds, even on an instance that sat idle. While UsageRail can't be reached, the instance keeps using the list it has; see API keys.
If the platform stops an instance before its after() work finishes, the counts it hadn't sent are lost and never billed. In our October 2026 tests on Vercel that didn't happen: every successful response was counted in a run of 60,000 requests at 100 a second, one of 120 requests a minute apart over two hours, and a burst of 70 requests at once that started 70 new instances.
Options
usageRail() takes appKey, auth, onKeyStoreUnavailable, staleKeySnapshot, onQuotaUnavailable and logger, which work as on the Express page, plus:
| Option | Default | What it does |
|---|---|---|
waitUntil | Required | What keeps work running after the response: after from next/server, or waitUntil from @vercel/functions. |
consumer | None | With auth: "custom", (request) => ({ id }) for the caller, or null for anonymous traffic. |
onReject | JSON error | (rejection, request) => Response to send your own response when a request is refused. |
next build works without USAGERAIL_APP_KEY, for builds that get their secrets only at run time. At run time a missing key throws as soon as the module loads.
Count units instead of requests
In apps that count units, call usage.track(n) in the handler with a whole number from 0 to 10,000,000. usage comes in the handler's second argument, next to params. Route weights and reservations work as described under quotas.
Use your own authentication
Tell UsageRail who the caller is from the request:
import { after } from "next/server";
import { usageRail } from "@usagerail/node/next";
import { accountIdFromRequest } from "@/lib/auth"; // your own check
export const rail = usageRail({
appKey: process.env.USAGERAIL_APP_KEY,
auth: "custom",
consumer: (request) => {
const accountId = accountIdFromRequest(request);
return accountId ? { id: accountId } : null;
},
waitUntil: after,
});consumer runs before your handler and has to return right away, not a promise. If finding the caller takes a lookup, do it in middleware.ts and pass the account ID on in a request header that the middleware sets.
Returning null makes the request anonymous: it's recorded, but not rate limited, not counted against a quota and never billed.
Rejected requests
Refused requests get the same statuses and JSON body as with Express; see the table on the Express page. To send your own body, return a response from onReject and keep the headers:
usageRail({
appKey: process.env.USAGERAIL_APP_KEY,
auth: "api-key",
waitUntil: after,
onReject: (rejection) =>
Response.json({ code: rejection.error }, { status: rejection.status, headers: rejection.headers }),
});What gets counted
- Each request is counted under its method, the template you gave
rail.route()and its outcome. - 2xx and 3xx responses are billable. 4xx and 5xx responses and refused requests are recorded but not billed.
redirect()andnotFound(), and the other helpers Next.js implements by throwing, are recorded with their real status: a 307 or 308 is billed, a 404 isn't. Any other error is recorded as a 500.- A client that disconnects while your handler runs is recorded as aborted and isn't billed.
- A server-sent events response counts once, when it starts, and is sent right away. Units you pass to
track()during the stream are added when it ends or the client disconnects.
Rate limits on many instances
Vercel runs as many instances as traffic needs, and each one counts rate limits on its own. With the default setting a customer can get the full limit on every instance. To keep the total close to what you set, choose Split the limit across servers; see rate limits.