Codeskill

Learn to code, step by step

Queues, background jobs, and long work

Moving slow work out of the HTTP request. Emails, image resizing, PDF generation, webhook calls. Users should not stare at a spinner while PHP sends twelve emails synchronously.

When to queue

  • Work takes more than a second reliably
  • Failure should retry without the user resubmitting
  • Traffic spikes would tie up all PHP-FPM workers

Do not queue everything. A quick INSERT belongs in the request. A “welcome” email with three attachments does not.

Job as a plain object

<?php
declare(strict_types=1);

namespace AppJobs;

final class SendWelcomeEmail
{
    public function __construct(
        public readonly int $userId,
        public readonly string $email,
    ) {}
}

Serializable, explicit, easy to log. The handler knows how to run it:

final class SendWelcomeEmailHandler
{
    public function __construct(private Mailer $mailer) {}

    public function handle(SendWelcomeEmail $job): void
    {
        $this->mailer->send($job->email, 'Welcome', '...');
    }
}

File-based queue (no Redis required)

For learning and small sites, a directory of JSON files works. Production often moves to Redis, database, or a managed queue – same job shape, different driver.

final class FileQueue
{
    public function __construct(private string $dir) {}

    public function push(object $job): void
    {
        $file = $this->dir . '/' . Uniqid('job_', true) . '.json';
        file_put_contents($file, json_encode([
            'class' => $job::class,
            'payload' => get_object_vars($job),
        ]), LOCK_EX);
    }

    public function pop(): ?array
    {
        $files = glob($this->dir . '/*.json') ?: [];
        sort($files);
        if ($files === []) {
            return null;
        }
        $file = $files[0];
        $data = json_decode(file_get_contents($file), true);
        unlink($file);
        return $data;
    }
}

The worker script

#!/usr/bin/env php
<?php
// bin/worker.php
require __DIR__ . '/../vendor/autoload.php';

$queue = new FileQueue(__DIR__ . '/../storage/queue');
$handlers = [
    SendWelcomeEmail::class => new SendWelcomeEmailHandler($mailer),
];

while ($job = $queue->pop()) {
    $handler = $handlers[$job['class']] ?? null;
    if (!$handler) {
        error_log('Unknown job: ' . $job['class']);
        continue;
    }
    $instance = new $job['class'](...$job['payload']);
    $handler->handle($instance);
}

Run via cron every minute, or use supervisor to keep a long-lived worker process. Same idea either way: pull, handle, repeat.

Dispatch from the controller

public function register(): void
{
    $userId = $this->users->create($_POST);
    $this->queue->push(new SendWelcomeEmail($userId, $_POST['email']));
    redirect('/dashboard', 'Account created.');
}

Retries and failures

Jobs fail. Network blips, API rate limits, full mailboxes. Track attempts on the job payload; after three failures, move to a failed/ folder and alert a human. Never silently drop work you promised to do.

Idempotency matters: sending the same welcome email twice because of a retry is annoying but survivable. Charging a card twice is not – design those jobs carefully.

PreviousSecurity deep dive – CSRF, XSS, headers, rate limiting