Securing RESTful APIs with Token Bucket Rate Limiting and APCu
A step-by-step guide to building a high-performance rate limiter for PHP web applications without an external Redis server.
Protecting API endpoints from excessive requests is a must for system developers. Common approaches overload a relational database or require a Redis cluster that adds operational cost — when in many cases local memory is more than enough.
Why Token Bucket, not a simple counter
Fixed-window counters are easy to build but have a well-known flaw. A limit of one hundred requests per minute can be bypassed with two hundred requests in two seconds, as long as the first hundred land at the end of one minute and the next hundred at the start of the next.
Token Bucket avoids this problem. Each client has a bucket with a fixed capacity that refills at a steady rate. Every request takes one token; if the bucket is empty, the request is rejected. The model allows brief bursts up to the bucket's capacity while capping the average rate over the long term.
Putting it in APCu
APCu is a shared memory cache that lives inside the PHP-FPM process itself. Because there is no network hop, a quota check completes in microseconds — compared with about a millisecond for Redis on a local network.
The update operation has to be atomic. Reading a value, computing, then writing it back opens a race window between workers running concurrently. Use the atomic increment operations APCu provides, or a lightweight lock, so two simultaneous requests do not both slip through a bucket that actually holds one token left.
Limits to know from the start
APCu is local to a single server. On one machine with many PHP-FPM workers, all workers share the same memory and this approach works perfectly. Once the application runs behind a load balancer with several machines, each machine has its own bucket — and the effective limit multiplies by the number of machines.
For that case there are two paths: accept the loosening and lower the per-machine threshold, or sync periodically to a persistent table so that heavy violations are still caught, even if not instantly.
Rejecting correctly
A rejection should answer with a 429 status and include the Retry-After header, so a well-behaved client knows when it may try again instead of retrying immediately and making things worse.
With tuned configuration, this architecture turns away bursts of tens of thousands of requests per second while keeping memory use small and predictable — with no additional process to run and monitor.
Budi Santoso
Contributing EditorSenior Software Architect dan pemerhati ekosistem PHP, cloud computing, dan performa web skala besar.
Related Articles
Lihat Semua →Observability on a Tight Budget: Structured Logs, Metrics, and Just Enough Tracing
20 Sep 2026
Implementing RFC 6238 TOTP Two-Factor Authentication (2FA) With No External Libraries
11 Sep 2026
Optimizing MySQL 8 on Shared Hosting
07 Sep 2026
The Anatomy of AI Image Generation: From Pixel Diffusion to Flow Matching
17 Oct 2026