I build UsageRail, which handles API keys, per-customer rate limits, quotas and usage billing for people who sell an API. It doesn't sit in front of the API as a gateway. An SDK runs inside your own route handlers and checks each request there, and the counts end up as usage on invoices in your own Stripe account.
On Next.js it looks like this:
// lib/usagerail.jsimport { 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,});// app/v1/items/[id]/route.jsimport { 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 Vercel-specific part was the counting. A serverless instance can be frozen between requests, so there's no background timer to flush counts. Instead the SDK uses after() (or waitUntil from @vercel/functions outside Next.js): after each response goes out, it sends the batched counts, picks up revoked keys and changed limits, and renews quota leases. One instance sends at most one batch every 0.5 seconds, and a batch that fails stays in memory for the next request.
The risk is that the platform stops an instance before its after() work finishes, and those counts are lost. In our October tests on Vercel that didn't happen: every successful response was counted in a run of 60,000 requests at 100 a second, in 120 requests a minute apart over two hours, and in a burst of 70 requests at once that started 70 new instances.
A few constraints we hit:
- It needs the Node.js runtime, since it uses
node:crypto. - Next.js doesn't tell a handler which file it came from, so you pass the route template (
/v1/items/[id]) yourself. Otherwise every ID would count as its own route. next buildworks without the app key, for projects that only get secrets at run time.
Docs for the Next.js setup: https://usagerail.com/docs/nextjs
I'd like to hear from anyone running metered APIs on Vercel, especially whether you've seen after() work get cut off under load.