Skip to main content
UpstashRedisRateLimiter is the Rate Limit component’s serverless backend. It builds on @upstash/ratelimit and Upstash Redis over HTTP REST to enforce distributed limits across instances and edge functions — no shared in-memory state. It implements the framework’s IRateLimiter interface, so the calling code is identical to any other backend.

Why Upstash

  • Distributed by default. Counters live in Upstash Redis, so the same limit holds across every instance and region.
  • Serverless-friendly. Talks to Upstash over HTTP REST, which works in edge functions and short-lived runtimes.
  • Three algorithms. Fixed window, sliding window, or token bucket — chosen with a single option.
  • Built-in analytics. Upstash rate-limit analytics are enabled by default for dashboard insight.
  • Same IRateLimiter interface. check, isLimited, reset, and getCount — identical to the other backends.

Installation

UpstashRedisRateLimiter ships with @ooneex/rate-limit and depends on the Upstash rate-limit and Redis clients.
bun add @ooneex/rate-limit @upstash/ratelimit @upstash/redis

Environment variables

VariableRequiredPurpose
RATE_LIMIT_UPSTASH_REDIS_URLYesUpstash Redis REST URL. Missing throws RateLimitException (CONFIG_REQUIRED).
RATE_LIMIT_UPSTASH_REDIS_TOKENYesUpstash Redis REST token. Missing throws RateLimitException (CONFIG_REQUIRED).
RATE_LIMIT_UPSTASH_REDIS_URL=https://your-db.upstash.io
RATE_LIMIT_UPSTASH_REDIS_TOKEN=your_rest_token
Both can also be passed through constructor options (url, token), which take precedence over the environment. They are validated when the limiter is constructed, so misconfiguration fails fast.

Options

UpstashRedisRateLimiter accepts an options object as its second constructor argument:
OptionTypeDefaultPurpose
urlstringRATE_LIMIT_UPSTASH_REDIS_URLOverride the REST URL.
tokenstringRATE_LIMIT_UPSTASH_REDIS_TOKENOverride the REST token.
algorithmUpstashAlgorithmTypesliding window, 120 / 60 sThe limiting algorithm and its parameters.
prefixstring"ratelimit"Redis key prefix; falls back to namespace.
namespacestring"ratelimit"Alias for prefix.
analyticsbooleantrueEnable Upstash rate-limit analytics.

Algorithms

The algorithm option is a tagged union — pick one and supply its parameters:
// Fixed window: limit requests per fixed time bucket
{ type: "fixedWindow", limit: 100, window: "60 s" }

// Sliding window (default): smoother limit over a rolling window
{ type: "slidingWindow", limit: 120, window: "60 s" }

// Token bucket: refillRate tokens per interval, up to maxTokens
{ type: "tokenBucket", refillRate: 10, interval: "10 s", maxTokens: 100 }
window and interval are Upstash Duration strings such as "60 s", "1 m", or "1 h". The default is a sliding window of 120 requests per 60 seconds.

How it works

check(key) calls the underlying limiter and normalizes the result into a RateLimitResultType:
FieldTypePurpose
limitedbooleanWhether this request exceeded the limit.
remainingnumberRequests left in the current window.
totalnumberThe configured limit.
resetAtDateWhen the window resets.
MethodPurpose
check(key)Consume one unit and return the full result.
isLimited(key)Convenience boolean — whether the key is over the limit.
reset(key)Reset the used tokens for a key; returns true on success.
getCount(key)Remaining requests for a key.

Usage

Key the limiter by whatever identifies the caller — an IP, a user id, or an API key.
import { container } from "@ooneex/container";
import { UpstashRedisRateLimiter } from "@ooneex/rate-limit";

const limiter = container.get(UpstashRedisRateLimiter);

const result = await limiter.check(`ip:${ip}`);
if (result.limited) {
  return context.response
    .status(429)
    .header(
      "Retry-After",
      String(Math.ceil((result.resetAt.getTime() - Date.now()) / 1000)),
    )
    .json({ error: "Too many requests", resetAt: result.resetAt });
}

return context.response.json({ remaining: result.remaining });
Configure a custom algorithm by subclassing the limiter:
import { decorator } from "@ooneex/rate-limit";
import { UpstashRedisRateLimiter } from "@ooneex/rate-limit";
import { AppEnv } from "@ooneex/app-env";
import { inject } from "@ooneex/container";

@decorator.rateLimit()
export class LoginRateLimiter extends UpstashRedisRateLimiter {
  constructor(@inject(AppEnv) env: AppEnv) {
    super(env, {
      prefix: "login",
      algorithm: { type: "fixedWindow", limit: 5, window: "1 m" },
    });
  }
}

Use in the app

In an @ooneex/app application, pass UpstashRedisRateLimiter to the rateLimiter slot of your App config. The framework registers it and exposes it as the "rateLimiter" container constant.
bun add @ooneex/app @ooneex/rate-limit @upstash/ratelimit @upstash/redis
import { App } from "@ooneex/app";
import { UpstashRedisRateLimiter } from "@ooneex/rate-limit";
import { TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger],
  rateLimiter: UpstashRedisRateLimiter,
});

await app.run();
With a rate limiter configured, the framework checks it before every route, keyed by the client IP — you don’t call check() yourself. When a request is over the limit it short-circuits with a 429 Too Many Requests and sets the standard headers automatically:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1719571200
If the limiter backend errors, the check fails open (the request proceeds) and the error is logged, so a transient Upstash outage doesn’t take your API down. To customize the algorithm or limits, register a subclass (see Usage) and pass that class to the rateLimiter slot instead. Set RATE_LIMIT_UPSTASH_REDIS_URL and RATE_LIMIT_UPSTASH_REDIS_TOKEN in your .env.yml (or environment).

Exceptions

UpstashRedisRateLimiter throws RateLimitException (HTTP 429) on misconfiguration or operation failures, carrying a machine-readable key.
KeyWhen
CONFIG_REQUIREDThe REST URL or token is missing from both options and the environment.
RATE_LIMIT_CHECK_FAILEDA check() call against Upstash failed.
RATE_LIMIT_RESET_FAILEDA reset() call failed.
RATE_LIMIT_COUNT_FAILEDA getCount() call failed.
import { RateLimitException } from "@ooneex/rate-limit";

try {
  const result = await limiter.check(`ip:${ip}`);
} catch (error) {
  if (error instanceof RateLimitException) {
    logger.error(`Rate limit error [${error.key}]: ${error.message}`);
  }
  throw error;
}

Best practices

  • Pick the algorithm for the job. Sliding window for smooth API limits, fixed window for simple per-minute caps, token bucket for burst-tolerant limits.
  • Key by the right identity. Rate-limit by user id for authenticated routes and by IP for public ones; use a distinct prefix per limiter.
  • Surface resetAt. Return a Retry-After header and 429 so clients back off correctly.
  • Keep credentials in the environment. Load the REST URL and token from .env; never hard-code them.
  • Share one Upstash database safely. Give each limiter its own prefix/namespace so independent limits don’t collide.
See the Rate Limit component for the full interface and the other backends.