✨ We just launched fimo.ai - an AI Website Builder to create websites in minutes - Try it now

Strapi plugin logo for Strapi Rate Limiter

Strapi Rate Limiter

Production-ready rate limiting for Strapi 5 — IP-based, identity-aware, with optional Redis backing.

thumbnail for Strapi Rate Limiter

strapi-plugin-rate-limit

Production-ready rate limiting for Strapi 5 — IP-based, identity-aware, with optional Redis backing.

Strapi 5 License

Features

  • Zero-config — works out of the box with sensible defaults (100 req/min, in-memory)
  • Per-route rules with glob pattern matching
  • Optional auth-aware middleware (rate limit by API token or user, not just IP)
  • Redis support with automatic in-memory fallback (insurance limiter)
  • Cloudflare CF-Connecting-IP support
  • Burst protection mode
  • Allowlisting by IP, API token ID, or user ID
  • Path exclusion via glob patterns
  • Threshold warnings in Strapi logs
  • Standard X-RateLimit-* response headers
  • Admin dashboard with real-time event monitoring

Installation

npm install strapi-plugin-rate-limit
# or
yarn add strapi-plugin-rate-limit

Enable the plugin in ./config/plugins.ts:

export default ({ env }) => ({
  'strapi-plugin-rate-limit': {
    enabled: true,
  },
});

That's it. The global middleware registers automatically and applies to all /api/* and /graphql routes with default settings (100 requests per minute, in-memory store).

!WARNING The default in-memory store is not shared across server instances. It is suitable for development and single-process deployments only. For production clusters, configure Redis.

Configuration

All options are optional. Pass them under config. Below is a full example showing every available option with example values — in practice you only need to include the options you want to change from the defaults.

./config/plugins.ts

export default ({ env }) => ({
  'strapi-plugin-rate-limit': {
    enabled: true,
    config: {
      // Global defaults applied to all routes unless overridden by a rule
      defaults: {
        limit: 100, // requests allowed per interval
        interval: '1m', // time window ('30s', '1m', '5m', '1h', etc.)
        blockDuration: 0, // seconds to block after limit exceeded (0 = no extra block)
      },

      // Redis — omit entirely to use in-memory store
      // Provide either `url` OR `host`/`port`, not both
      redis: {
        url: env('REDIS_URL'), // e.g. 'redis://localhost:6379' or 'rediss://...'
        // host: env('REDIS_HOST', 'localhost'),
        // port: env.int('REDIS_PORT', 6379),
        // password: env('REDIS_PASSWORD'),
        tls: true, // required for Upstash, AWS ElastiCache, etc.
      },

      // Per-route overrides — first matching rule wins (supports glob patterns)
      rules: [
        { path: '/api/auth/**', limit: 5, interval: '15m', blockDuration: 300 },
        { path: '/api/upload', limit: 10, interval: '1m' },
        { path: '/api/articles', limit: 50, interval: '1m' },
      ],

      // Bypass rate limiting entirely for specific clients
      allowlist: {
        ips: ['127.0.0.1', '10.0.0.0/8'], // IP addresses or CIDR ranges
        tokens: ['3'], // API token IDs (as strings)
        users: ['1'], // User IDs (as strings)
      },

      // Paths to skip entirely — no rate limiting, no headers
      exclude: ['/api/health', '/api/metrics', '/api/webhooks/**'],

      // Use CF-Connecting-IP header (when behind Cloudflare)
      cloudflare: false,

      // Log a warning when a client reaches this % of their limit (0 = disabled)
      thresholdWarning: 0.8,

      // Prefix for rate limiter storage keys (useful when sharing a Redis instance)
      keyPrefix: 'rl',

      // Spread request delays evenly instead of allowing bursts then blocking
      execEvenly: false,
      execEvenlyMinDelayMs: 0,

      // In-memory blocking layer (Redis mode only) — rejects repeat offenders
      // from memory without hitting Redis
      inMemoryBlock: {
        enabled: true,
        consumedThreshold: 0, // points consumed to trigger block (0 = 2x the limit)
        duration: '1m', // how long the in-memory block lasts
      },

      // Burst mode — secondary token bucket that allows short bursts above the limit
      burst: {
        enabled: false,
        points: 10, // extra points for the burst window
        duration: '10s', // burst window duration
      },

      // Mask client IPs in admin dashboard events (last octet replaced with ***)
      maskClientIps: true,

      // How often the admin dashboard polls for new data
      adminPollInterval: '10s',
    },
  },
});

Configuration Reference

defaults

OptionTypeDefaultDescription
limitnumber100Requests allowed per interval
intervalstring'1m'Time window ('30s', '1m', '1h', etc.)
blockDurationnumber0Seconds to block after limit exceeded (0 = no block, max 86400)

redis

Leave redis unconfigured to use the in-memory store. Provide either url or host/port — not both.

OptionTypeDefaultDescription
urlstringRedis connection URL (redis:// or rediss://)
hoststringRedis hostname (alternative to url)
portnumberRedis port (1–65535)
passwordstringRedis password
tlsbooleanfalseEnable TLS (required for Upstash and most managed Redis)

When Redis is configured, the plugin automatically creates an insurance limiter — an in-memory fallback that activates if Redis becomes unreachable, so rate limiting keeps working during outages.

rules

Array of per-route overrides. Each rule requires path, limit, and interval. An optional blockDuration can override the global default per rule. Paths support glob patterns via picomatch.

OptionTypeRequiredDescription
pathstringYesGlob pattern to match request paths
limitnumberYesRequests allowed per interval
intervalstringYesTime window ('30s', '1m', '1h', etc.)
blockDurationnumberNoSeconds to block after limit exceeded (overrides global, max 86400)
rules: [
  { path: '/api/auth/**', limit: 5, interval: '15m', blockDuration: 300 },
  { path: '/api/articles', limit: 50, interval: '1m' },
  { path: '/api/upload', limit: 10, interval: '1m' },
];

The first matching rule wins. Unmatched paths fall back to defaults.

allowlist

OptionTypeDefaultDescription
ipsstring[][]IP addresses or CIDR ranges to bypass rate limiting
tokensstring[][]API token IDs to bypass rate limiting
usersstring[][]User IDs to bypass rate limiting

The ips list supports both exact addresses and CIDR notation:

allowlist: {
  ips: [
    '127.0.0.1',        // exact IPv4
    '10.0.0.0/8',       // IPv4 CIDR range
    '::1',              // exact IPv6
    '2001:db8::/32',    // IPv6 CIDR range
  ],
},

Token and user allowlisting requires the route-level middleware.

exclude

Array of path patterns (glob) to skip entirely. Excluded paths receive no rate limiting and no headers.

exclude: ['/api/health', '/api/metrics', '/api/webhooks/**'];

inMemoryBlock

Fast local blocking layer for Redis mode. When a client far exceeds the limit, subsequent requests are rejected from memory without hitting Redis.

OptionTypeDefaultDescription
enabledbooleantrueEnable in-memory blocking
consumedThresholdnumber0Points consumed to trigger block (0 = 2× the limit)
durationstring'1m'How long the in-memory block lasts

Other Options

OptionTypeDefaultDescription
thresholdWarningnumber0.8Log a warning when usage hits this ratio (0–1, 0 = disabled)
keyPrefixstring'rl'Prefix for rate limiter keys (useful when sharing a Redis instance)
cloudflarebooleanfalseUse CF-Connecting-IP header for client IP
execEvenlybooleanfalseDistribute delay evenly across requests instead of all at once
execEvenlyMinDelayMsnumber0Minimum delay (ms) between requests when execEvenly is on
maskClientIpsbooleantrueMask client IPs in admin dashboard events (last octet → ***)
adminPollIntervalstring'10s'How often the admin dashboard polls for updated status and events

burst

Allows short bursts above the normal limit using a secondary token bucket.

OptionTypeDefaultDescription
enabledbooleanfalseEnable burst protection
pointsnumber0Extra points for the burst window
durationstring'10s'Burst window duration

Route-Level Middleware

The global middleware rate-limits by IP address. If you want auth-aware rate limiting (by API token or user ID), add the route-level middleware to specific routes.

Identity resolution priority:

  1. API Tokentoken:{id}
  2. Authenticated Useruser:{id}
  3. IP (fallback) → skipped (already handled by global middleware)

Add it in your route configuration:

// src/api/article/routes/article.ts
export default {
  routes: [
    {
      method: 'POST',
      path: '/articles',
      handler: 'article.create',
      config: {
        middlewares: ['plugin::strapi-plugin-rate-limit.rate-limit'],
      },
    },
  ],
};

When an authenticated request hits this route, the middleware applies a separate rate limit keyed to the token or user identity. This means a single user can't exhaust the IP-level quota for everyone behind a shared IP.

Reverse Proxy

If Strapi is behind a reverse proxy (Nginx, Caddy, etc.), enable Koa's proxy trust setting so ctx.request.ip resolves correctly:

./config/server.ts

export default ({ env }) => ({
  proxy: {
    koa: true,
  },
});

Without this, all requests may appear to come from 127.0.0.1.

Cloudflare

If you're behind Cloudflare, enable the cloudflare option to read the real client IP from the CF-Connecting-IP header:

config: {
  cloudflare: true,
}

!WARNING When cloudflare: true is set, the CF-Connecting-IP header is trusted unconditionally. Your server must be exclusively behind Cloudflare, and your firewall must block direct access to Strapi's port from the public internet. If clients can reach Strapi directly, they can spoof this header and bypass IP-based rate limiting entirely.

Redis Setup

!NOTE Redis is strongly recommended for production. Without it, each server process maintains its own counters, so rate limits won't be enforced correctly behind a load balancer.

Connection URL

redis: {
  url: env('REDIS_URL'), // redis://localhost:6379
}

Host / Port

redis: {
  host: env('REDIS_HOST', 'localhost'),
  port: env.int('REDIS_PORT', 6379),
  password: env('REDIS_PASSWORD'),
}

TLS (Upstash, AWS ElastiCache, etc.)

redis: {
  url: env('REDIS_URL'), // rediss://...
  tls: true,
}

Insurance Limiter

When Redis is active, an in-memory insurance limiter runs alongside it. If the Redis connection drops, rate limiting continues against the in-memory store until Redis recovers. No configuration needed — this is automatic.

Response Headers

Every rate-limited response includes standard headers:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp (seconds) when the window resets

When the limit is exceeded (HTTP 429), the response also includes:

HeaderDescription
Retry-AfterSeconds to wait before retrying

The 429 response body follows the Strapi error format:

{
  "data": null,
  "error": {
    "status": 429,
    "name": "TooManyRequestsError",
    "message": "Too many requests, please try again later.",
    "details": {}
  }
}

Admin Dashboard

The plugin adds a dashboard in the Strapi admin under Plugins → Rate Limiter. It includes:

  • Status overview — strategy (Memory/Redis with connection status), default limits, custom rule count, and allowlist counts
  • Event monitoring — a live table of recent blocked and warning events showing the client, path, source, usage, and reset time
  • Auto-refresh — the dashboard polls every 10 seconds for new data
  • Disabled state — when the plugin is not enabled, the dashboard shows a helpful message instead of an error

Events are stored in an in-memory ring buffer (last 100 entries) and are recorded whenever a request is blocked (429) or crosses the warning threshold. This works with both memory and Redis strategies.

!NOTE By default, client IPs displayed in the admin dashboard are masked (e.g. ip:192.168.1.***) to reduce PII exposure. Server-side logs (strapi.log.warn) always show the full IP. Set maskClientIps: false in the plugin config to show full IPs in the dashboard.

License

MIT

Install now

npm install strapi-plugin-rate-limit

STATS

1 GitHub starNot downloaded this week

Last updated

11 days ago

Strapi Version

5.35.0 and above

Author

github profile image for bolg55
bolg55

Useful links

Create your own plugin

Check out the available plugin resources that will help you to develop your plugin or provider and get it listed on the marketplace.