10

I am working on an app that requires fetching data from a third-party server and that server allows max 1 request per seconds.

Now, all request send as job and I am trying to implement Laravel "Rate Limiting" to release 1 job per second but unable to figure out why it should be implemented and there is no real-life example in the web.

Did anyone implement it?

Any hint of this?

Tarek Adam
  • 3,387
  • 3
  • 27
  • 52
Meathanjay
  • 2,023
  • 1
  • 18
  • 24
  • Make your own internal API call to consume the external API, use the job to consume your own internal API. Rate limit your own internal API. – Ohgodwhy Nov 20 '17 at 19:37
  • I am doing something similar right now and my idea is this: When you dispatch the job to the queue, store the dispatch datetime in cache. For all dispatches check the cache to see when the last job was dispatched and if it is less than 1 second then use `Job::dispatch()->delay($lastDispatchDateTime->addSeconds(1));` – Emanuel S. Mar 07 '19 at 10:56

6 Answers6

7

I'm the author of mxl/laravel-queue-rate-limit Composer package.

It allows you to rate limit jobs on specific queue without using Redis.

  1. Install it with:

    $ composer require mxl/laravel-queue-rate-limit:^1.0
    
  2. This package is compatible with Laravel 5.5+ and uses auto-discovery feature to add MichaelLedin\LaravelQueueRateLimit\QueueServiceProvider::class to providers.

  3. Add rate limit settings to config/queue.php:

    'rateLimit' => [
        'mail' => [
            'allows' => 1,
            'every' => 5
        ]
    ]
    

    These settings allow to run 1 job every 5 seconds on mail queue. Make sure that default queue driver (default property in config/queue.php) is set to any value except sync.

  4. Run queue worker with --queue mail option:

    $ php artisan queue:work --queue mail
    

    You can run worker on multiple queues, but only queues referenced in rateLimit setting will be rate limited:

    $ php artisan qeueu:work --queue mail,default
    

    Jobs on default queue will be executed without rate limiting.

  5. Queue some jobs to test rate limiting:

    SomeJob::dispatch()->onQueue('mail');
    SomeJob::dispatch()->onQueue('mail');
    SomeJob::dispatch()->onQueue('mail');
    SomeJob::dispatch();
    
mixel
  • 25,177
  • 13
  • 126
  • 165
1

spatie/laravel-rate-limited-job-middleware

This is a nice package if you are using laravel 6 or above. Nice thing is you can configure middleware in the job.

Install

composer require spatie/laravel-rate-limited-job-middleware

namal
  • 1,164
  • 1
  • 10
  • 15
0

Assuming you have only single worker you can do something like this:

  • do what has to be done
  • get time (with microseconds)
  • sleep time that is 1s minus difference between finish time and start time

so basically:

doSomething()
$time = microtime(true);
usleep(1000 - ($time - LARAVEL_START));
Marcin Nabiałek
  • 109,655
  • 42
  • 258
  • 291
0

If you need "throttling" and are not using Redis as your queue driver you can try to use the following code:

public function throttledJobDispatch( $delayInSeconds = 1 ) 
{
   $lastJobDispatched = Cache::get('lastJobDispatched');

   if( !$lastJobDispatched ) {
      $delay_until = now();
   } else { 
      if ($lastJobDispatched->addSeconds($delayInSeconds) < now()) {
         $delay_until = now();
      } else {
         $delay_until = $lastJobDispatched->addSeconds($delayInSeconds);
      }
   }
   Job::dispatch()->onQueue('YourQueue')->delay($delay_until);
   Cache::put('lastJobDispatched', $delay_until, now()->addYears(1) );
}

What this code does is release a job to the queue and set the start time X seconds after the last dispatched job's start time. I successully tested this with database as queue-driver and file as cache driver.

There are two minor problems I have encountered so far:

1) When you use only 1 second as a delay, depending on your queue worker - the queue worker may actually only "wake up" once every couple of seconds. So, if it wakes up every 3 seconds, it will perform 3 jobs at once and then "sleep" 3 seconds again. But on average you will still only perform one job every second.

2) In Laravel 5.7 it is not possible to use Carbon to set the job delay to less than a second because it does not support milli- or microseconds yet. That should be possible with Laravel 5.8 - just use addMilliseconds instead of addSeconds.

Emanuel S.
  • 150
  • 5
0

I had the exact same issue recently. Laravel's out-of-the-box job rate limiters do not allow you to set an execution limit the per second level.

I handled it by writing a custom piece of job middleware like this:

<?php

namespace App\Jobs\Middleware;

use Illuminate\Support\Facades\Redis;

class RedisRateLimited
{
    /**
     * Uses Redis to throttle the execution of a job.
     *
     * @param int $allow count of jobs you wish to allow to execute
     * @param int $every period of time in seconds
     */
    public function __construct(protected int $allow, protected int $every)
    {}

    /**
     * Process the queued job.
     *
     * @param  mixed  $job
     * @param  callable  $next
     * @return mixed
     */
    public function handle($job, $next)
    {
        Redis::throttle('your-key-here')
            ->block(0)->allow($this->allow)->every($this->every)
            ->then(function () use ($job, $next) {
                // Lock obtained...
                $next($job);
            }, function () use ($job) {
                // Could not obtain lock...
                $job->release(5);
            });
    }
}

Then attach it to the job in question like this:

/**
 * Get the middleware the job should pass through.
 *
 * @return array
 */
public function middleware()
{
    return [new RedisRateLimited(allow: 1, every: 1)];
}
eysikal
  • 579
  • 1
  • 6
  • 13
-2

You could use this package to use rate limiting with Redis or another source, like a file. Uses settings to set bucket size and rate as fractions of the time limit, so very small storage.

composer require bandwidth-throttle/token-bucket

https://github.com/bandwidth-throttle/token-bucket

It allows you to wrap the check in an if, so it will wait for a free token to be available, 1 a minute in your example. In effect, it makes the service sleep for the required amount of time until a new minute.

tristanbailey
  • 4,427
  • 1
  • 26
  • 30