Skip to content

Rate Limiting

Enforce rate limits using the session/ratelimit package with CoreAPI policies.

Architecture

Request → JWT Middleware → Rate Limit Middleware → Handler
              │                    │
              │                    ├── Extract client_id from JWT
              │                    ├── Look up policy from PolicyStore
              │                    └── Check/update rate limit counters
              └── Set claims in context (including azp/client_id)

Quick Start

import (
    "github.com/grokify/coreforge/coreapi"
    "github.com/grokify/coreforge/session/ratelimit"
)

// 1. Create policy store with policies
policyStore := coreapi.NewMemoryPolicyStore()
policyStore.CreatePolicy(ctx, coreapi.StandardPolicy)
policyStore.CreatePolicy(ctx, coreapi.PremiumPolicy)
policyStore.SetDefaultPolicy(ctx, "standard")

// 2. Bind clients to policies
policyStore.BindClientToPolicy(ctx, "mobile-app", "standard")
policyStore.BindClientToPolicy(ctx, "partner-api", "premium")

// 3. Create rate limiter with CoreAPI resolver
limiter := ratelimit.New(
    ratelimit.NewMemoryStorage(),
    ratelimit.WithResolver(ratelimit.NewCoreAPIResolver(policyStore)),
    ratelimit.WithKeyFunc(ratelimit.CompositeKey()),
)

// 4. Apply middleware
router.Use(jwtMiddleware)         // Extract JWT claims
router.Use(limiter.Middleware())  // Enforce rate limits

Storage Options

Memory Storage

For development and single-instance deployments:

storage := ratelimit.NewMemoryStorage()

Uses sliding window algorithm with automatic cleanup of expired entries.

Redis Storage

For distributed deployments:

storage := ratelimit.NewRedisStorage(
    redisClient,
    ratelimit.WithRedisKeyPrefix("ratelimit:"),
)

Uses Lua scripts for atomic operations. Requires github.com/redis/go-redis/v9.

Key Functions

Key functions extract the rate limit key from the request context.

PrincipalKey

Rate limit per user/principal:

ratelimit.WithKeyFunc(ratelimit.PrincipalKey())
// Key: "pid:user-uuid"

ClientKey

Rate limit per OAuth client/application:

ratelimit.WithKeyFunc(ratelimit.ClientKey())
// Key: "azp:mobile-app"

CompositeKey

Rate limit per user per application (recommended):

ratelimit.WithKeyFunc(ratelimit.CompositeKey())
// Key: "pid:user-uuid:azp:mobile-app"

Limit Resolvers

Resolvers determine the rate limit for each request.

CoreAPI Resolver

Look up limits from PolicyStore based on client_id:

resolver := ratelimit.NewCoreAPIResolver(policyStore,
    ratelimit.WithResolverCacheDuration(5 * time.Minute),
)

limiter := ratelimit.New(storage,
    ratelimit.WithResolver(resolver),
)

Static Resolver

Apply the same limit to all requests:

limiter := ratelimit.New(storage,
    ratelimit.WithStaticLimit(ratelimit.Limit{
        Requests: 100,
        Window:   time.Minute,
    }),
)

Rate Limit Headers

The middleware adds standard headers to responses:

Header Description
X-RateLimit-Limit Maximum requests allowed
X-RateLimit-Remaining Requests remaining in window
X-RateLimit-Reset Unix timestamp when limit resets
Retry-After Seconds to wait (only on 429)

Handling Rate Limit Exceeded

When limits are exceeded, the middleware returns:

  • Status: 429 Too Many Requests
  • Body: JSON error with details
  • Headers: Rate limit headers including Retry-After
{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Try again in 30 seconds.",
  "retry_after": 30
}

Options

limiter := ratelimit.New(storage,
    // Use CoreAPI resolver
    ratelimit.WithResolver(resolver),

    // Key extraction from JWT
    ratelimit.WithKeyFunc(ratelimit.CompositeKey()),

    // Custom error handler
    ratelimit.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
        // Custom error response
    }),

    // Skip rate limiting for certain requests
    ratelimit.WithSkipper(func(r *http.Request) bool {
        return r.URL.Path == "/health"
    }),
)

Full Example

package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/grokify/coreforge/coreapi"
    "github.com/grokify/coreforge/session/jwt"
    "github.com/grokify/coreforge/session/ratelimit"
)

func main() {
    // Policy store setup
    policyStore := coreapi.NewMemoryPolicyStore()
    policyStore.CreatePolicy(ctx, coreapi.FreePolicy)
    policyStore.CreatePolicy(ctx, coreapi.StandardPolicy)
    policyStore.CreatePolicy(ctx, coreapi.PremiumPolicy)
    policyStore.SetDefaultPolicy(ctx, "free")

    // Bind OAuth clients to policies
    policyStore.BindClientToPolicy(ctx, "mobile-app", "standard")
    policyStore.BindClientToPolicy(ctx, "enterprise-client", "premium")

    // Rate limiter
    limiter := ratelimit.New(
        ratelimit.NewMemoryStorage(),
        ratelimit.WithResolver(ratelimit.NewCoreAPIResolver(policyStore)),
        ratelimit.WithKeyFunc(ratelimit.CompositeKey()),
    )

    // Router
    r := chi.NewRouter()
    r.Use(jwt.Middleware(jwtConfig))
    r.Use(limiter.Middleware())

    r.Get("/api/resource", handleResource)

    http.ListenAndServe(":8080", r)
}

JWT Claims

The rate limiter extracts claims from JWT tokens set by the JWT middleware:

Claim Use
pid (principal ID) Identifies the user
azp (authorized party) Identifies the OAuth client

Ensure your JWT middleware sets these claims in the request context.

Next Steps