This is a threadsafe implementation of the Token Bucket algorithm in PHP. You can use a token bucket to limit an usage rate for a resource (e.g. a stream bandwidth or an API usage).
The token bucket is an abstract metaphor which doesn't have a direction of the resource consumption. I.e. you can limit a rate for consuming or producing. E.g. you can limit the consumption rate of a third party API service, or you can limit the usage rate of your own API service.
Use Composer:
composer require bandwidth-throttle/token-bucket
The package is in the namespace
bandwidthThrottle\tokenBucket
.
This example will limit the rate of a global resource to 10 requests per second for all requests.
use bandwidthThrottle\tokenBucket\Rate;
use bandwidthThrottle\tokenBucket\TokenBucket;
use bandwidthThrottle\tokenBucket\storage\FileStorage;
$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate = new Rate(10, Rate::SECOND);
$bucket = new TokenBucket(10, $rate, $storage);
$bucket->bootstrap(10);
if (!$bucket->consume(1, $seconds)) {
http_response_code(429);
header(sprintf("Retry-After: %d", floor($seconds)));
exit();
}
echo "API response";
Note: In this example TokenBucket::bootstrap()
is part of the code. This is
not recommended for production, as this is producing unnecessary storage
communication. TokenBucket::bootstrap()
should be part of the application's
bootstrap or deploy process.
First you need to decide the scope of your resource. I.e. do you want to limit
it per request, per user or amongst all requests? You can do this by choosing a
Storage
implementation of the desired scope:
-
The
RequestScope
limits the rate only within one request. E.g. to limit the bandwidth of a download. Each requests will have the same bandwidth limit. -
The
SessionScope
limits the rate of a resource within a session. The rate is controlled over all requests of one session. E.g. to limit the API usage per user. -
The
GlobalScope
limits the rate of a resource for all processes (i.e. requests). E.g. to limit the aggregated download bandwidth of a resource over all processes. This scope permits race conditions between processes. The TokenBucket is therefore synchronized on a shared mutex.
When you have your storage you can finally instantiate a
TokenBucket
.
The first parameter is the capacity of the bucket. I.e. there will be never
more tokens available. This also means that consuming more tokens than the
capacity is invalid.
The second parameter is the token-add-Rate
.
It determines the speed for filling the bucket with tokens. The rate is the
amount of tokens added per unit, e.g. new Rate(100, Rate::SECOND)
would add 100 tokens per second.
The third parameter is the storage, which is used to persist the token amount of the bucket. The storage does determine the scope of the bucket.
A token bucket needs to be bootstrapped. While the method
TokenBucket::bootstrap()
doesn't have any side effects on an already bootstrapped bucket, it is not
recommended do call it for every request. Better include that in your
application's bootstrap or deploy process.
Now that you have a bootstrapped bucket, you can start consuming tokens. The
method TokenBucket::consume()
will either return true
if the tokens were consumed or false
else.
If the tokens were consumed your application can continue to serve the resource.
Else if the tokens were not consumed you should not serve the resource.
In that case consume()
did write a duration of seconds into its second parameter
(which was passed by reference). This is the duration until sufficient
tokens would be available.
In the first example we did either serve the request or fail with the HTTP status code 429. This is actually a very resource efficient way of throtteling API requests as it doesn't reserve resources on your server.
However sometimes
it is desirable not to fail but instead wait a little bit and then continue
serving the requests. You can do this by consuming the token bucket with
a BlockingConsumer
.
use bandwidthThrottle\tokenBucket\Rate;
use bandwidthThrottle\tokenBucket\TokenBucket;
use bandwidthThrottle\tokenBucket\BlockingConsumer;
use bandwidthThrottle\tokenBucket\storage\FileStorage;
$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate = new Rate(10, Rate::SECOND);
$bucket = new TokenBucket(10, $rate, $storage);
$consumer = new BlockingConsumer($bucket);
$bucket->bootstrap(10);
// This will block until one token is available.
$consumer->consume(1);
echo "API response";
This will effectively limit the rate to 10 requests per seconds as well. But in this case the client has not to bother with the 429 error. Instead the connection is just delayed to the desired rate.
This project is free and under the WTFPL. Responsible for this project is Markus Malkusch markus@malkusch.de.
If you like this project and feel generous donate a few Bitcoins here: 1335STSwu9hST4vcMRppEPgENMHD2r1REK