Guide · 7 min read

How to implement per-user rate limits in your AI app

Per-user rate limits for AI apps need atomic enforcement, plan awareness, and refundable reservations. Here is the pattern that works under load - using @vevee/sdk and ~30 lines of code.

Last updated: 2026-05-10

Most "rate limit" libraries (Upstash Redis, Cloudflare WAF, express-rate-limit) gate by IP or user ID against a fixed window - fine for hitting an HTTP endpoint, wrong for gating an AI call.

AI apps need rate limits that are: per-end-user (not per-IP), plan-aware (free vs paid get different limits), atomic under concurrency (two parallel calls can't both pass), and refundable when the AI call fails.

This guide walks through the right pattern.

Step-by-step

1. Decide on the unit and window

Common combos: requests/minute (anti-abuse), images/day (cost cap), tokens/month (margin cap). You can stack multiple - Vevee applies all matching limit groups to a single event.

2. Define limit groups in the dashboard

Create a limit group per cap. For example: "anti-abuse: 60 requests / minute / user", "cost cap: 100 image renders / month / user", "margin cap: 100k tokens / month / user". Match rules route events into them.

3. Reserve before the AI call

reserve() atomically holds quota. If allowed=false, return a clean 429 to the caller - do not call OpenAI / Replicate / etc.

const r = await vevee.reserve(userId, "image.render", 1, { model: "flux-pro" });
if (!r.allowed) {
  return new Response(
    JSON.stringify({ error: "rate_limited", reasons: r.reasons }),
    { status: 429 },
  );
}

4. Commit on success, release on failure

Wrap the AI call in try/catch. On success, commit. On failure, release. The reservation auto-releases after 60 seconds if your code crashes.

try {
  const result = await callAiProvider();
  await vevee.commit(r.reservationId!);
  return result;
} catch (err) {
  await vevee.release(r.reservationId!);
  throw err;
}

5. Surface remaining quota in your response headers

Conventional rate-limited APIs return X-RateLimit-Remaining and X-RateLimit-Reset headers. Read those from the reservation's counters and include them in your response. Clients can self-throttle.

const counter = r.counters[0];
const headers = new Headers();
headers.set("X-RateLimit-Limit", String(counter.quota));
headers.set("X-RateLimit-Remaining", String(counter.remaining));
if (counter.resetsAt) {
  headers.set("X-RateLimit-Reset", counter.resetsAt);
}

Plan-aware limits

Free users get tighter limits than paid users. Implement this by setting different quotas per plan, then assigning each user the right plan via vevee.upsertSubscription. When they upgrade, you change the plan; counters and rules adjust automatically.

Stacking short and long windows

You can have a "60 requests / minute" anti-abuse limit AND a "1000 requests / month" cost cap on the same plan. Both are checked. The user is blocked if either is at quota. Vevee handles the stacking - your code does not need a separate library per window.

Avoiding accidental DoS on yourself

A subtle gotcha: your API endpoint itself may have its own connection-pool or worker limits. Vevee gates AI calls but does not gate ingress. Pair Vevee quotas with a Cloudflare or Vercel rate limit on the ingress to protect your own infrastructure from abuse.

When to use a separate Redis-based rate limiter

For sub-second per-IP anti-DDoS bursts, a Redis-based rate limiter is still faster (lower latency, no network hop to Vevee). The two coexist: ingress rate limit at the edge for 100 req/sec, Vevee quotas for cost-relevant per-user limits.

Frequently asked questions

Can I rate-limit by IP or do I have to use a user ID?

Vevee is per-user-id. For pre-auth endpoints, pass a stable identifier like the IP or session ID as the userId. After auth, switch to the real user ID.

How does this compare to express-rate-limit or @upstash/ratelimit?

Those are great for short-window per-IP HTTP rate limiting. Vevee is for plan-aware, long-window, refundable-on-failure quotas tied to AI events. Use both.

How fast is reserve() - does it add latency?

Reserve is a single HTTP call to Vevee - typically 30-100ms p99 from a EU/US region. If your AI call takes 2-10 seconds, this is a rounding error.

What if I want to allow unlimited usage on a plan?

Set the limit group quota to a very high number, or omit limit groups from that plan entirely. canUse and reserve return allowed=true when no limit group matches AND the plan opts in to unmatched_event allow.

Other guides

8 min · Guide

How to track OpenAI API usage by user (with quotas, in real time)

Step-by-step guide to tracking OpenAI API usage per end-user with real-time quotas. Pure-TypeScript pattern using @vevee/sdk. Concurrency-safe, provider-agnostic, ten-minute integration.

9 min · Guide

How to implement usage-based pricing for an AI product

Decide between per-token, per-action, and hybrid pricing. Implement quotas, refunds, and Stripe meter sync. Step-by-step with code, from a developer who has shipped all three models.

6 min · Guide

The reserve / commit / release pattern: atomic AI quota enforcement

Why naive AI usage metering breaks under concurrency, and the only correct pattern that fixes it. Reserve / commit / release explained, with full TypeScript example and the failure modes it prevents.

8 min · Guide

How to charge AI app users by token usage (with refunds and live balance)

Step-by-step: charge users for actual token consumption with pre-paid credits, post-paid invoicing, and a live balance display. Atomic reservations, accurate refunds, and Stripe meter sync.

10 min · Guide

Build a freemium image generator: end-to-end tutorial

Ship a freemium AI image generator with hard caps, upgrade prompts, real-time analytics, and a live "renders left" badge - using @vevee/sdk + your image provider of choice. Full code, ten-minute build.

12 min · Guide

Add cookieless traffic attribution to a Next.js app

Six copy-paste steps to attribute every signup and every order to the visit that brought it, with @vevee/sdk 1.14. The visit lives in browser memory, a server route forwards events with the secret key, and nothing is written to the device.

8 min · Guide

Send revenue from your payment webhook to Vevee

Record paid orders and refunds with vevee.revenue.order and vevee.revenue.refund so every channel and campaign gets a revenue column. Covers amounts in minor units net of tax, idempotency on the provider id, refunds that arrive early, and a webhook that never fails because of analytics.

7 min · Guide

Build the two attribution funnels from JSON

Paste one JSON file into Vevee's funnel import to get a visit to signup funnel and a signup to paid funnel, both split by channel. Covers the split forms, step filters, units, and how to read the split table.

9 min · Guide

Test attribution with fake traffic in test mode

Check a cookieless attribution setup before real visitors arrive: a probe script that hits every call once, a seed script that fills test mode with believable traffic and refuses anything but an sk_test_ key, and a cross-check of the visit funnel against the Traffic page.

Set your first limit in about five minutes.

50k operations free every month. No card required.
Join waitlist