Ratelock

Engines

Upstash Redis Engine

Edge-compatible rate limiting powered by Upstash Redis over HTTP.

@ratelock/upstash is the HTTP-native Redis engine for RateLock. It uses the official @upstash/redis client, so it works in serverless and Edge runtimes where a TCP Redis client cannot open a connection.

The adapter exposes the same four strategies, result contracts, and resilience policies as @ratelock/redis.

Choose the transport for your runtime

Use @ratelock/upstash for Edge or serverless handlers. Use @ratelock/redis with node-redis, ioredis, or Bun's native client when your process can keep a TCP connection open.

Installation

npm install @ratelock/upstash @upstash/redis
# or
pnpm add @ratelock/upstash @upstash/redis

Configure the REST endpoint and token through your deployment's secret manager:

UPSTASH_REDIS_REST_URL=https://your-database.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-secret-token

@upstash/redis is a peer dependency. Keep the standard token private; it can execute writes and Lua scripts.

Quick start

import { Redis } from '@upstash/redis'
import { fixedWindow } from '@ratelock/upstash'

const redis = Redis.fromEnv()

const limiter = await fixedWindow({
    client: redis,
    limit: 100,
    windowMs: 60_000,
    prefix: 'api',
})

const result = await limiter.check('user:123')

The factory receives an existing Upstash client. Constructing the client and keeping credentials outside RateLock makes the same adapter usable with environment variables, framework bindings, or a custom secret manager.

Cloudflare Workers and Pages use the provider-specific Upstash entry point:

import { Redis } from '@upstash/redis/cloudflare'

const redis = Redis.fromEnv(env)

Use the standard @upstash/redis entry point shown above for Node.js-based serverless platforms and Edge platforms that support it. The RateLock adapter only depends on the shared REST command surface.

Edge and serverless handlers

Initialize the limiter once per warm isolate and reuse the promise between requests:

import { Redis } from '@upstash/redis'
import { fixedWindow } from '@ratelock/upstash'

const limiterPromise = fixedWindow({
    client: Redis.fromEnv(),
    limit: 100,
    windowMs: 60_000,
})

export default async function handler(request: Request): Promise<Response> {
    const limiter = await limiterPromise
    const id = request.headers.get('x-user-id') ?? 'anonymous'
    const result = await limiter.check(id)

    const resetSeconds = Math.max(0, Math.ceil((result.reset - Date.now()) / 1000))
    const headers = {
        'RateLimit-Remaining': String(result.remaining),
        'RateLimit-Reset': String(resetSeconds),
    }

    if (!result.allowed) {
        return new Response('Too Many Requests', {
            status: 429,
            headers: { ...headers, 'Retry-After': String(resetSeconds) },
        })
    }

    return new Response('OK', { headers })
}

For Cloudflare module workers, bindings arrive through the env argument. Use the Cloudflare entry point and memoize the limiter for the warm isolate:

import { Redis } from '@upstash/redis/cloudflare'
import { fixedWindow } from '@ratelock/upstash'

type Env = {
    UPSTASH_REDIS_REST_URL: string
    UPSTASH_REDIS_REST_TOKEN: string
}

let limiterPromise: ReturnType<typeof fixedWindow> | undefined

function getLimiter(env: Env) {
    return (limiterPromise ??= fixedWindow({
        client: Redis.fromEnv(env),
        limit: 100,
        windowMs: 60_000,
    }))
}

export default {
    async fetch(request: Request, env: Env): Promise<Response> {
        const limiter = await getLimiter(env)
        const result = await limiter.check(request.headers.get('x-user-id') ?? 'anonymous')
        return new Response(result.allowed ? 'OK' : 'Too Many Requests', {
            status: result.allowed ? 200 : 429,
            headers: { 'RateLimit-Remaining': String(result.remaining) },
        })
    },
}

See the framework recipes for a Next.js Edge middleware example and identifier guidance behind proxies.

Configuration options

All Upstash limiters accept these shared base options:

Prop

Type

The strategy-specific options are the same as the other RateLock engines:

FixedWindowLimiterConfig

Prop

Type

Built-in resilience policies

Every strategy accepts the same resilience configuration:

const limiter = await fixedWindow({
    client: Redis.fromEnv(),
    limit: 100,
    windowMs: 60_000,
    cache: { maxSize: 1000, ttlMs: 30_000 },
    retry: { maxAttempts: 3 },
    circuitBreaker: { failureThreshold: 5, recoveryTimeoutMs: 30_000 },
    fallback: 'allow',
})
OptionDescription
cacheCache denied results in the current isolate to reduce repeated REST calls.
retryRetry transient Upstash or network failures with exponential backoff.
circuitBreakerTemporarily stop calling Upstash after consecutive failures.
fallbackChoose whether backend failures throw, allow, or deny.

The package also re-exports withCache, withRetry, withCircuitBreaker, and withFallback for standalone composition.

Batch checks and atomicity

checkBatch uses one Upstash HTTP pipeline and preserves the order of the input identifiers. Upstash pipelines are not transactions: commands from other clients can interleave between pipeline entries. Each individual Lua script invocation is still atomic, which protects the state transition for a single identifier.

Cleanup

await limiter.destroy()

destroy() is intentionally a no-op for this engine. The REST client owns no TCP socket that RateLock should close. The Redis client remains owned by your application.

When to use

  • Cloudflare Workers, Vercel Edge, and other fetch-based serverless handlers
  • Applications that already use Upstash Redis
  • Distributed limits without managing a Redis TCP connection
  • Deployments where a warm isolate can reuse the client and limiter promise

For a long-lived Node.js or Bun process with a connection-oriented Redis deployment, compare the Redis engine instead.

How is this guide?

Last updated on

On this page