add-rate-limiting-to-apis

A TypeScript rule for limiting how many API requests each client can make during a configured time window.

In plain words
What is it for?
Use it to add request quotas, remaining-request status, reset times, and retry information to APIs.
Why use it?
It prevents one client from using too many requests and makes over-limit failures explicit.

Cursor rule

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add rules/pauljphilp/effectpatterns/add-rate-limiting-to-apis
Clone the repo
git clone --depth 1 https://github.com/PaulJPhilp/EffectPatterns
Per session 1,218 This file is loaded in full into every session.
When invoked 1,218 The same file — it is already loaded in full.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.01218 $0.01218
Opus 5 $0.00609 $0.00609
Sonnet 5 $0.00244 $0.00244
Haiku 4.5 $0.00122 $0.00122

Measured yesterday against content hash 957648beef79, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

add-rate-limiting-to-apis scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured yesterday.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

content/published/rules/cursor/add-rate-limiting-to-apis.mdc · 198 lines

How it starts

The opening of the file, as written. The whole thing — 198 lines — stays where its author put it; the contents beside it link to each section on GitHub.

description: Use a rate limiter service to enforce request quotas per client. globs: "**/*.ts" alwaysApply: true

Add Rate Limiting to APIs

Rule: Use a rate limiter service to enforce request quotas per client.

Example

import { Effect, Context, Layer, Ref, HashMap, Data, Duration } from "effect"
import { HttpServerRequest, HttpServerResponse } from "@effect/platform"

// ============================================
// 1. Define rate limit types
// ============================================

interface RateLimitConfig {
  readonly maxRequests: number
  readonly windowMs: number
}

interface RateLimitState {
  readonly count: number
  readonly resetAt: number
}

class RateLimitExceededError extends Data.TaggedError("RateLimitExceededError")<{
  readonly retryAfter: number
  readonly limit: number
}> {}

// ============================================
// 2. Rate limiter service
// ============================================

interface RateLimiter {
  readonly check: (key: string) => Effect.Effect<void, RateLimitExceededError>
  readonly getStatus: (key: string) => Effect.Effect<{
    remaining: number
    resetAt: number
  }>
}

class RateLimiterService extends Context.Tag("RateLimiter")<
  RateLimiterService,
  RateLimiter
>() {}

// ============================================
// 3. In-memory rate limiter implementation
// ============================================

const makeRateLimiter = (config: RateLimitConfig) =>
  Effect.gen(function* () {
    const state = yield* Ref.make(HashMap.empty<string, RateLimitState>())

    const getOrCreateState = (key: string, now: number) =>
      Ref.modify(state, (map) => {
        const existing = HashMap.get(map, key)

        if (existing._tag === "Some") {
          // Check if window expired
          if (now >= existing.value.resetAt) {
            // Start new window
            const newState: RateLimitState = {
              count: 0,
              resetAt: now + config.windowMs,
            }
            return [newState, HashMap.set(map, key, newState)]
          }
          return [existing.value, map]
        }

        // Create new entry
        const newState: RateLimitState = {
          count: 0,
          resetAt: now + config.windowMs,
        }
        return [newState, HashMap.set(map, key, newState)]
      })

    const incrementCount = (key: string) =>
      Ref.modify(state, (map) => {
        const existing = HashMap.get(map, key)
        if (existing._tag === "Some") {
          const updated = { ...existing.value, count: existing.value.count + 1 }
          return [updated.count, HashMap.set(map, key, updated)]
        }
        return [1, map]
      })

    const limiter: RateLimiter = {
      check: (key) =>
        Effect.gen(function* () {
          const now = Date.now()
          const currentState = yield* getOrCreateState(key, now)

          if (currentState.count >= config.maxRequests) {
            const retryAfter = Math.ceil((currentState.resetAt - now) / 1000)
            return yield* Effect.fail(
              new RateLimitExceededError({
                retryAfter,
                limit: config.maxRequests,
              })
            )
          }

          yield* incrementCount(key)
        }),

      getStatus: (key) =>
        Effect.gen(function* () {
          const now = Date.now()
          const currentState = yield* getOrCreateState(key, now)
          return {
            remaining: Math.max(0, config.maxRequests - currentState.count),
            resetAt: currentState.resetAt,
          }
        }),
    }

    return limiter
  })

// ============================================
// 4. Rate limit middleware
// ============================================

const withRateLimit = <A, E, R>(
  handler: Effect.Effect<A, E, R>
): Effect.Effect<
  A | HttpServerResponse.HttpServerResponse,
  E,
  R | RateLimiterService | HttpServerRequest.HttpServerRequest
> =>
  Effect.gen(function* () {
    const request = yield* HttpServerRequest.HttpServerRequest
    const rateLimiter = yield* RateLimiterService

    // Use IP address as key (in production, might use user ID or API key)
    const clientKey = request.headers["x-forwarded-for"] || "unknown"

    const result = yield* rateLimiter.check(clientKey).pipe(
      Effect.matchEffect({
        onFailure: (error) =>
          Effect.succeed(
            HttpServerResponse.json(
              {
                error: "Rate limit exceeded",
                retryAfter: error.retryAfter,
              },
              {
                status: 429,
                headers: {
                  "Retry-After": String(error.retryAfter),
                  "X-RateLimit-Limit": String(error.limit),
                  "X-RateLimit-Remaining": "0",
                },
              }
            )
          ),
        onSuccess: () => handler,
      })
    )

    return result
  })

// ============================================
// 5. Usage example
// ============================================

const RateLimiterLive = Layer.effect(
  RateLimiterService,
  makeRateLimiter({
    maxRequests: 100,      // 100 requests
    windowMs: 60 * 1000,   // per minute
  })
)

const apiEndpoint = withRateLimit(
  Effect.gen(function* () {
    // Your actual handler logic
    return HttpServerResponse.json({ data: "Success!" })
  })
)

Read the full file on GitHub · 198 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. yesterday First seen · 198 lines · 1,218 tokens per session scan A 957648beef79

Subscribe to this mod's changes

add-rate-limiting-to-apis is a cursor rule published in the GitHub repository PaulJPhilp/EffectPatterns (794 stars, last pushed 2mo ago), licensed MIT. It adds 1,218 tokens to every session, about $0.0061 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.