Skip to main content

Overview

The Upstash Rate Limit provider implements distributed rate limiting for your Contract Kit applications using Upstash Redis and the @upstash/ratelimit library. It provides a serverless-friendly solution for protecting your API from abuse.

Features

  • ✅ Implements the standard RateLimitPort interface
  • ✅ Uses Upstash Redis REST API (serverless-friendly, no connection pooling needed)
  • ✅ Fixed window rate limiting algorithm
  • ✅ Dynamic rate limit configuration per request
  • ✅ Configurable key prefix to avoid collisions
  • ✅ Type-safe configuration with Zod validation
  • ✅ Works seamlessly with Contract Kit framework and middleware

Installation

npm install @contract-kit/provider-rate-limit-upstash @upstash/redis @upstash/ratelimit

Configuration

Set these environment variables:
VariableRequiredDescriptionExample
UPSTASH_REDIS_REST_URLYesYour Upstash Redis REST URLhttps://us1-properly-ancient-12345.upstash.io
UPSTASH_REDIS_REST_TOKENYesYour Upstash Redis REST tokenAXXXeyJpZCI6IjEy...
UPSTASH_PREFIXNoKey prefix for rate limit keys (default: ck:ratelimit)myapp:ratelimit

Getting Upstash Credentials

  1. Sign up at Upstash
  2. Create a new Redis database
  3. Navigate to the database details page
  4. Copy the REST URL and REST token from the “REST API” section

Example .env

UPSTASH_REDIS_REST_URL=https://us1-properly-ancient-12345.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXXXeyJpZCI6IjEy...
UPSTASH_PREFIX=myapp:ratelimit

Setup

Basic Setup

import { createServer } from "@contract-kit/server";
import { upstashRateLimitProvider } from "@contract-kit/provider-rate-limit-upstash";

export const app = createServer({
  ports: basePorts,
  providers: [upstashRateLimitProvider],
  // ... rest of your config
});

Type Your Ports

To get proper type inference for the rate limit port, extend your ports type:
import type { RateLimitPort } from "@contract-kit/provider-rate-limit-upstash";

// Your base ports
const basePorts = definePorts({
  db: dbAdapter,
});

// After using upstashRateLimitProvider, your ports will have this shape:
type AppPorts = typeof basePorts & {
  rateLimit: RateLimitPort;
};

API Reference

The provider extends your ports with a rateLimit property:

hit(options)

Check if a request should be rate limited.
const result = await ctx.ports.rateLimit.hit({
  key: `ip:${ctx.ip}`,
  limit: 100,
  windowSec: 60, // 100 requests per 60 seconds
});

if (result.limited) {
  // Request should be rejected
}
Parameters:
  • options.key: Unique identifier for the rate limit (e.g., IP address, user ID)
  • options.limit: Maximum number of requests allowed
  • options.windowSec: Time window in seconds
Returns: Promise<RateLimitResult>
interface RateLimitResult {
  limited: boolean;      // true if the request should be rejected
  remaining: number | null;  // requests remaining in the window
  resetAt: Date | null;      // when the window resets
}

Usage Examples

Rate Limiting by IP Address

// Example middleware that rate limits by IP address
async function rateLimitMiddleware(ctx: AppCtx) {
  const result = await ctx.ports.rateLimit.hit({
    key: `ip:${ctx.ip}`,
    limit: 100,
    windowSec: 60, // 100 requests per 60 seconds
  });

  if (result.limited) {
    return {
      status: 429,
      headers: {
        "X-RateLimit-Limit": "100",
        "X-RateLimit-Remaining": String(result.remaining ?? 0),
        "X-RateLimit-Reset": result.resetAt?.toISOString() ?? "",
      },
      body: {
        error: "Too Many Requests",
        message: "Rate limit exceeded. Please try again later.",
      },
    };
  }

  // Request is allowed
  return undefined; // continue to next middleware/handler
}

Different Rate Limits for Different Endpoints

You can apply different rate limits for different operations:
// Strict rate limit for auth endpoints
const loginResult = await ctx.ports.rateLimit.hit({
  key: `login:${ctx.ip}`,
  limit: 5,
  windowSec: 300, // 5 attempts per 5 minutes
});

// More relaxed rate limit for API endpoints
const apiResult = await ctx.ports.rateLimit.hit({
  key: `api:user:${userId}`,
  limit: 1000,
  windowSec: 3600, // 1000 requests per hour
});

Using with Contract Metadata

You can define rate limit metadata on your contracts:
const getTodos = contract.get("/todos")
  .meta({
    rateLimit: { key: "api", limit: 60, windowSec: 60 },
  });
Then in your middleware, read this metadata and apply rate limits:
async function rateLimitFromMeta(ctx: AppCtx, meta?: any) {
  if (!meta?.rateLimit) return;

  const { key, limit, windowSec } = meta.rateLimit;
  const result = await ctx.ports.rateLimit.hit({
    key: `${key}:${ctx.userId ?? ctx.ip}`,
    limit,
    windowSec,
  });

  if (result.limited) {
    throw ctx.err.TooManyRequests();
  }
}

Rate Limiting by User ID

async function userRateLimit(ctx: AppCtx) {
  if (!ctx.userId) return; // Skip for unauthenticated requests

  const result = await ctx.ports.rateLimit.hit({
    key: `user:${ctx.userId}`,
    limit: 1000,
    windowSec: 3600, // 1000 requests per hour per user
  });

  if (result.limited) {
    return {
      status: 429,
      body: {
        error: "Rate limit exceeded",
        resetAt: result.resetAt,
      },
    };
  }
}

Implementation Details

  • Algorithm: Uses fixed window rate limiting via Ratelimit.fixedWindow()
  • Backend: Upstash Redis REST API (serverless-compatible)
  • Per-request configuration: Creates a new Ratelimit instance for each hit() call to support dynamic limits
  • Key prefix: Configurable prefix to avoid key collisions

Advanced Usage

Access the Underlying Redis Client

The provider extends the standard RateLimitPort with access to the underlying Upstash Redis client:
import type { UpstashRateLimitPort } from "@contract-kit/provider-rate-limit-upstash";

const rateLimit = ctx.ports.rateLimit as UpstashRateLimitPort;

// Access the Redis client for advanced operations
await rateLimit.client.get("some:key");
await rateLimit.client.set("some:key", "value");

Best Practices

Use clear, hierarchical keys like api:user:123 or login:ip:192.168.1.1 for better debugging.
Balance security and user experience. Too strict limits frustrate users, too lenient limits won’t protect your API.
Return X-RateLimit-* headers to inform clients about their rate limit status.
Use stricter limits for sensitive operations like login attempts and authentication.
Apply different rate limits for authenticated users vs anonymous requests.

Common Rate Limit Strategies

Conservative (Strict)

// Good for sensitive operations
await ctx.ports.rateLimit.hit({
  key: `login:${ctx.ip}`,
  limit: 5,
  windowSec: 300, // 5 attempts per 5 minutes
});

Moderate

// Good for general API usage
await ctx.ports.rateLimit.hit({
  key: `api:${ctx.userId}`,
  limit: 100,
  windowSec: 60, // 100 requests per minute
});

Generous

// Good for high-throughput operations
await ctx.ports.rateLimit.hit({
  key: `read:${ctx.userId}`,
  limit: 1000,
  windowSec: 3600, // 1000 requests per hour
});

Next Steps