piljac1's avatar
Level 28

Mail/Notification delay not precise ?

Hi everyone! I encountered a pretty weird issue today. I had to implement some Mail/Notification throttling today in order to respect our mail service provider's limit per second.

This is a very simplified test of what I have:

class TestMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Test Mail',
        );
    }

    /**
     * Get the message content definition.
     */
    public function content(): Content
    {
        return new Content(
            view: 'emails.test',
        );
    }

    /**
     * Get the attachments for the message.
     *
     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
     */
    public function attachments(): array
    {
        return [];
    }
}
for ($i = 0; $i < 10; $i++) {
    $mail = (new TestMail)->delay($i);

    Mail::to('[email protected]')->send($mail);
}

Expected behavior : One mail is sent per second.

Actual behavior (queue output) :

  2023-05-24 18:25:51 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:51 App\Mail\TestMail ......................... 33.32ms DONE
  2023-05-24 18:25:55 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:55 App\Mail\TestMail ......................... 13.70ms DONE
  2023-05-24 18:25:55 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:55 App\Mail\TestMail ......................... 22.25ms DONE
  2023-05-24 18:25:55 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:55 App\Mail\TestMail ......................... 19.24ms DONE
  2023-05-24 18:25:55 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:55 App\Mail\TestMail ......................... 14.25ms DONE
  2023-05-24 18:25:58 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:58 App\Mail\TestMail ......................... 12.84ms DONE
  2023-05-24 18:25:58 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:58 App\Mail\TestMail ......................... 46.36ms DONE
  2023-05-24 18:25:59 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:59 App\Mail\TestMail ......................... 16.89ms DONE
  2023-05-24 18:25:59 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:25:59 App\Mail\TestMail ......................... 25.09ms DONE
  2023-05-24 18:26:02 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:26:02 App\Mail\TestMail ......................... 13.40ms DONE

As you can see there are sometimes a substential delay between mails, sometimes none at all.

However, if you increase the delay to 5 seconds ($i * 5), the behavior is somewhat ok, but you can still see some lack of precision (sometimes it's 3 seconds between, sometimes 6 seconds).

  2023-05-24 18:29:10 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:10 App\Mail\TestMail ......................... 35.21ms DONE
  2023-05-24 18:29:16 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:17 App\Mail\TestMail ......................... 22.50ms DONE
  2023-05-24 18:29:20 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:20 App\Mail\TestMail ......................... 14.34ms DONE
  2023-05-24 18:29:26 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:26 App\Mail\TestMail ......................... 21.55ms DONE
  2023-05-24 18:29:33 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:33 App\Mail\TestMail ......................... 15.66ms DONE
  2023-05-24 18:29:36 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:36 App\Mail\TestMail ......................... 16.54ms DONE
  2023-05-24 18:29:42 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:42 App\Mail\TestMail ......................... 14.01ms DONE
  2023-05-24 18:29:46 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:46 App\Mail\TestMail ......................... 17.23ms DONE
  2023-05-24 18:29:52 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:52 App\Mail\TestMail ......................... 23.15ms DONE
  2023-05-24 18:29:55 App\Mail\TestMail .............................. RUNNING
  2023-05-24 18:29:55 App\Mail\TestMail ......................... 12.44ms DONE

Is it a precision limitation of the framework or am I missing something ?

P.S. The current "workaround" I have in place is this :

->delay(floor($i / $emailsPerSecond) * $delayBetweenBatches)

Basically, it will send X emails per seconds but will introduce a X second delay before the next "batch" starts. In this context, the "batch" I'm talking about is an actual Batch instance, it is just the raw concept of adding a delay after a certain amount of dispatched notifications. It's not perfect because I'm losing some performance by having to wait X seconds between each "batches", but it's what I settled with for now.

0 likes
3 replies
LaryAI's avatar
Level 58

The issue is likely due to the fact that the delay method in Laravel queues the job for execution after the specified delay, but it does not guarantee that the job will be executed exactly after the specified delay. This is because the queue worker may be busy processing other jobs, or there may be other factors that affect the timing of job execution.

One possible solution is to use a more precise timing mechanism, such as the PHP sleep function, to introduce a delay between sending each email. For example:

for ($i = 0; $i < 10; $i++) {
    $mail = new TestMail();

    Mail::to('[email protected]')->send($mail);

    sleep(1); // wait for 1 second before sending the next email
}

This will ensure that each email is sent exactly 1 second after the previous one. However, this approach may not be suitable if you need to send a large number of emails, as it may take a long time to send them all.

Another possible solution is to use a queue with a higher concurrency limit, so that more jobs can be processed simultaneously. This can help to reduce the delay between sending each email. For example, you could use the database queue driver with a higher concurrency limit:

php artisan queue:work --queue=mail --tries=3 --delay=0 --max-time=3600 --max-jobs=10 --concurrency=5

This will start a queue worker that processes jobs from the "mail" queue, with a concurrency limit of 5. This means that up to 5 jobs can be processed simultaneously, which can help to reduce the delay between sending each email. However, this approach may require more resources, as more jobs are being processed simultaneously.

Snapey's avatar
Snapey
Best Answer
Level 122

as Lary says, you need to consider what else is going on. The delay says "process this job after ..."

you have no control about the exact timing, it just depends when a worker is available

1 like
piljac1's avatar
Level 28

@Snapey Makes sense, I guess I was so focused and tunnel visionned about the delay feature that I didn't think about the bigger picture. I'll probably create a Throttleable trait and respective interface to add a usleep call within the notifications/mails instances so I can efficiently respect the configured limit. So a 10 emails per second limit would introduce a 100ms usleep in each throttleable notifications. It will actually be a little bit slower than 10 per second due to the other applicative processing time, but it should be close enough (and at least it won't be unpredictable and bust it). Thanks for your input!

1 like

Please or to participate in this conversation.