Master WordPress REST API rate limiting using the token bucket algorithm. Learn to protect your endpoints from spikes with high-performance Redis storage.
Last month, a client’s headless storefront started throwing 503 errors under a sudden marketing surge. Their custom endpoints were hammering the database, and the standard WordPress heartbeat wasn’t enough to stop the bleeding. I had to implement a custom gatekeeper in less than three hours.
If you’re relying on simple transient-based blocking, you’re likely hitting a race condition wall. Here is how I architected a production-grade solution using the token bucket algorithm to secure the WordPress REST API.
The token bucket algorithm is elegant because it allows for bursts while maintaining a strict average rate. Imagine a bucket that holds a maximum number of tokens. Every time a request hits your API, it tries to consume a token. If the bucket is empty, the request is rejected. If tokens exist, the request proceeds, and the token count decrements.
The "refill" happens at a constant rate. Unlike a leaky bucket, which processes at a fixed speed, this allows for transient traffic spikes—provided the bucket isn't already dry.
We first tried using set_transient() to track request counts per IP. It failed almost immediately. WordPress transients are stored in the wp_options table (or an object cache). Under high concurrency, wp_options becomes a massive bottleneck due to row-level locking. We saw latency jump by about 450ms per request during peak load.
If you are dealing with high traffic, you must offload this counter to an atomic store like Redis. If you are already optimizing your stack, WordPress performance: Database-level request coalescing for REST API might help you handle the queries that actually get through, but the gatekeeping needs to happen before the database is ever touched.
To implement this, I hook into rest_api_init. We need a storage mechanism that supports atomic increments. Redis is the industry standard here.
PHP#6A9955">// Simple Token Bucket Logic class RateLimiter { private $redis; public function __construct($redis) { $this->redis = $redis; } public function check_limit($ip, $limit, $refill_rate) { $key = "rl:{$ip}"; $now = microtime(true); #6A9955">// Use a Lua script for atomicity to prevent race conditions $script = ' local bucket = redis.call("hmget", KEYS[1], "tokens", "last_refill") local tokens = tonumber(bucket[1]) or 10 local last_refill = tonumber(bucket[2]) or ARGV[1] local delta = math.max(0, (ARGV[1] - last_refill) * ARGV[2]) tokens = math.min(10, tokens + delta) if tokens >= 1 then redis.call("hmset", KEYS[1], "tokens", tokens - 1, "last_refill", ARGV[1]) return 1 else return 0 end '; return $this->redis->eval($script, [$key, $now, $refill_rate], 1); } }
By using a Lua script executed within Redis, we guarantee that the read-modify-write cycle is atomic. No two requests will ever "see" the same token count and decrement it simultaneously.
Once you have the logic, hooking it into the WordPress ecosystem is straightforward. Use the rest_pre_dispatch filter to intercept requests before they hit your controllers.
PHPadd_filter('rest_pre_dispatch', function($result, $server, $request) { $ip = $_SERVER['REMOTE_ADDR']; $limiter = new RateLimiter(get_redis_connection()); if (!$limiter->check_limit($ip, 10, 0.5)) { return new WP_Error('too_many_requests', 'Slow down!', ['status' => 429]); } return $result; }, 10, 3);
My first attempt at this implementation used a standard PHP get and set approach. It worked in staging but failed in production because PHP’s execution isn’t atomic regarding external storage. The Lua script was the missing piece.
Also, don't forget to handle X-Forwarded-For headers if you are behind a load balancer or Cloudflare. If you don't, you'll end up rate-limiting your own gateway instead of the malicious actors.
If your plugin is handling complex data mutations, ensure your rate limiting doesn't interfere with your transaction integrity. I often pair this with WordPress REST API Idempotency: Building Reliable Plugin Mutations to ensure that even if a request is throttled or retried, the state remains consistent.
1. Does this work on shared hosting? Usually, no. Most shared environments don't provide a persistent Redis instance. If you're on shared hosting, you're better off using a managed WAF like Cloudflare to handle rate limiting at the edge.
2. What happens if Redis goes down?
Your check_limit method should fail open. If the Redis connection throws an exception, catch it and return true (allow the request). It’s better to have a vulnerable site for a few minutes than a site that is completely offline.
3. Is the token bucket algorithm better than a fixed window? Yes. Fixed windows (e.g., 100 requests per hour) allow users to exhaust their quota in the first minute. Token buckets smooth out that traffic, which is much kinder to your database.
This approach has saved me countless hours of on-call stress. If you're building high-traffic endpoints, don't rely on the default WP behavior. Build your own gate, use atomic storage, and keep your database happy. Next time, I’d probably look into moving this logic entirely into an Nginx limit_req module to keep the traffic away from PHP-FPM entirely, but for now, the Redis-backed approach gives me the control I need.
WordPress performance hinges on efficient data delivery. Learn to implement Stale-While-Revalidate caching for the REST API to ensure instant, scalable responses.
Read moreImprove WordPress performance by implementing database-level request coalescing. Stop N+1 query storms in your REST API and hydrate responses efficiently.