Asynchronous/Concurrent Patterns in PHP

Course 3 · Ch 8
Asynchronous/Concurrent Patterns in PHP: Queues, Background Jobs
Handling slow work without making a visitor wait for it — and why PHP's request model makes this genuinely different from other languages

A typical PHP script runs from start to finish within a single request, then the process ends. Sending a welcome email, generating a large PDF report, or resizing an uploaded image can take seconds — far too slow to make a visitor wait for, and risky to do directly inside the request/response cycle at all.

PHP's Request Model — Why This Matters

Node.js / long-running processes

  • One process stays alive, handling many requests over time
  • Genuine in-process async/await is natural

Traditional PHP (with most web servers)

  • Each request typically starts a fresh process/script execution
  • No natural way to "keep working after the response is sent" reliably

This is exactly why PHP relies on a separate mechanism — a job queue, backed by a process that runs independently of any web request — rather than genuine in-request asynchronous code, for anything that needs to happen "in the background."

The Queue Pattern

Web Request pushes job Queue (DB/Redis) Response sent immediately picks up Worker process
The request returns immediately; a separate worker process handles the slow work whenever it gets to it

Instead of doing slow work directly, the request "pushes" a description of the job (what needs doing, with what data) onto a queue — typically a database table or Redis — and responds to the visitor right away. A completely separate, long-running worker process continuously checks the queue and processes jobs as they appear.

A Minimal Database-Backed Queue

<?php // Pushing a job — inside a normal web request, e.g. after user registration function queueEmail(PDO $pdo, string $to, string $subject) { $stmt = $pdo->prepare("INSERT INTO jobs (type, payload, status) VALUES ('send_email', :payload, 'pending')"); $stmt->execute(['payload' => json_encode(['to' => $to, 'subject' => $subject])]); } // In the registration controller: queueEmail($pdo, $user['email'], 'Welcome!'); jsonResponse(['message' => 'Registered successfully'], 201); // responds immediately — the email hasn't actually been sent yet ?>

The Worker — A Long-Running Separate Script

<?php // worker.php — run via CLI, kept running continuously (e.g. by supervisord/systemd) while (true) { $stmt = $pdo->query("SELECT * FROM jobs WHERE status = 'pending' LIMIT 1"); $job = $stmt->fetch(); if ($job) { $payload = json_decode($job['payload'], true); try { sendEmail($payload['to'], $payload['subject']); // the genuinely slow operation markJobComplete($pdo, $job['id']); } catch (Exception $e) { markJobFailed($pdo, $job['id'], $e->getMessage()); // Chapter 3-style exception handling, applied to a worker } } else { sleep(2); // nothing to do — wait briefly before checking again } } ?>

The worker runs entirely outside the web server — typically started once and kept alive indefinitely by a process supervisor — repeatedly polling for new jobs. This is a fundamentally different execution model from a normal web request, which is exactly why it needs to be designed and run separately.

A failed job must be handled deliberately, not silently dropped
Marking a job 'failed' (rather than leaving it stuck as 'pending' forever, or deleting it) preserves a record of what went wrong and allows a retry strategy to be built — exactly the kind of deliberate error handling from Intermediate Chapter 3, applied to a background process instead of a request.

Why Not Just Use a Separate Thread?

PHP doesn't have built-in multi-threading the way some other languages do (true threads sharing memory within one process). The queue pattern sidesteps this entirely — instead of threads sharing memory, completely separate PHP processes communicate only through the queue (a database row, or a message broker like Redis), which is simpler to reason about and naturally survives a worker crashing or restarting.

Real-World Tooling

This chapter's example is intentionally minimal to show the underlying mechanism clearly. Real projects typically use a dedicated queue library — Laravel's built-in Queue system (Chapter 6), or standalone packages — which add retry logic, delayed jobs, and proper worker process management on top of the same basic pattern demonstrated here.

Coding Challenges

Challenge 1

Design (in writing) the SQL schema for a "jobs" table suitable for the queue pattern shown in this chapter — list each column, its type, and a one-sentence reason it's needed. Include at minimum: an id, a job type, a payload, a status, and a created timestamp.

📄 View solution
Challenge 2

Write a queueJob(PDO $pdo, string $type, array $payload) function that inserts a new pending job with a JSON-encoded payload, generalising the chapter's queueEmail() to handle any job type. Then show how it would be called for two different job types: 'send_email' and 'resize_image'.

📄 View solution
Challenge 3

Extend the worker loop from this chapter to dispatch to a different function depending on the job's "type" column (using match()), supporting both 'send_email' and 'resize_image' job types, each with its own try/catch and markJobComplete/markJobFailed calls. Explain in a comment what would happen if one job type's handler threw an uncaught exception, with no try/catch around the dispatch at all.

📄 View solution

Chapter 8 Quick Reference

  • PHP's request model — typically one script run per request, no natural in-process background work
  • Queue pattern — push a job description, respond immediately, a separate worker processes it later
  • Jobs table / Redis — common backing stores for a queue
  • Worker — a long-running CLI script, kept alive by a process supervisor, polling for pending jobs
  • Failed jobs — mark deliberately, don't silently drop; enables retry strategies
  • No native threads — separate processes + a shared queue, rather than shared-memory threading
  • Real projects use dedicated tooling (Laravel Queues, etc.) — this chapter shows the underlying mechanism
  • Next chapter: capstone — a small REST API with auth, database, tests