Skip to Content
Course content

243: Message Queues Between PHP Services (RabbitMQ)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was consulting for a startup that had built a fairly standard e-commerce platform. Everything was fine until they hit a growth spurt. Their checkout process was a single, massive PHP script: it processed the payment, updated the inventory, generated a PDF invoice, and sent a confirmation email—all before sending the final "Thank You" page to the user. One Tuesday, their third-party email provider started lagging. Suddenly, users were staring at a white screen for 30 seconds, getting timed out by Nginx, and—because the page didn't load—they were hitting 'Refresh' and accidentally placing the same order three times. It was a nightmare.

The problem wasn't the PHP code; it was the architecture. We were forcing the user to wait for tasks that didn't actually need to happen in real-time. This is exactly why we use message queues like RabbitMQ. Instead of doing everything right now, the web request simply drops a "note" (a message) into a queue and tells the user, "We've got your order!" and finishes the request in milliseconds. A separate PHP process, running in the background, picks up those notes and does the heavy lifting at its own pace.

Decoupling the Request from the Heavy Lifting

In a typical PHP environment, your script dies the moment the HTML is sent to the browser. You can't just start a background thread and hope for the best. RabbitMQ acts as the middleman—the "Broker"—that lives outside your PHP process. When your web app wants to trigger a slow task, it acts as a Producer. It sends a message to an Exchange, which then routes it into a Queue.

The magic happens when you run a Consumer. This is just another PHP script, but instead of being triggered by a URL, it runs as a long-lived CLI process (usually managed by something like Supervisor). It sits there, listening to the queue, and processes messages as they arrive. If your email provider goes down again, the messages just sit safely in RabbitMQ until the service comes back online. Your users never see a spinning wheel, and your database doesn't lock up.

Wiring Up the Producer and Consumer

To get this working in PHP, you'll likely use the php-amqplib library. I've found that the trickiest part for most developers is understanding that the Producer shouldn't care who is reading the message. It just needs to know which exchange to send it to.

// The Producer (Inside your Controller)
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

// We declare the queue to make sure it exists before we send to it
$channel->queue_declare('order_processing', false, true, false, false);

$data = json_encode(['order_id' => 12345, 'user_email' => 'dev@example.com']);
$msg = new AMQPMessage($data, ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);

$channel->basic_publish($msg, '', 'order_processing');

$channel->close();
$connection->close();

Now, the Consumer is a separate file that you run from the terminal. I always recommend wrapping your consumer logic in a try-catch block; if the consumer crashes and you aren't using a process manager, your queue will just grow indefinitely without anyone processing it.

// The Consumer (Run via CLI: php consumer.php)
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('order_processing', false, true, false, false);

echo " [*] Waiting for orders. To exit press CTRL+C\n";

$callback = function ($msg) {
    $order = json_decode($msg->body, true);
    echo " [x] Processing Order: ", $order['order_id'], "\n";
    
    // Simulate a slow task like sending an email or generating a PDF
    sleep(5); 
    
    echo " [x] Done!\n";
    // This tells RabbitMQ the message was handled and can be deleted
    $msg->ack(); 
};

$channel->basic_consume('order_processing', '', false, false, false, false, $callback);

while ($channel->is_consuming()) {
    $channel->wait();
}

Handling Reliability and Acknowledgments

One detail I can't stress enough: always use manual acknowledgments (the $msg->ack() call). By default, RabbitMQ can be configured to remove a message the second it's sent to a consumer. But what happens if your consumer script hits a fatal error or the server reboots halfway through processing an order? If you've already "acknowledged" the message, that order is gone forever. By using manual ACKs, RabbitMQ will wait for your script to confirm success. If the connection drops before the ACK is sent, RabbitMQ simply puts the message back in the queue for another consumer to try again. It's a simple safety net that saves you from massive data loss.




📋 Practical Task

Build an Asynchronous User Welcome Sequence

Instead of making a new user wait for three different API calls during registration, you are going to build a decoupled welcome system. Your goal is to ensure the registration request finishes instantly, while the "heavy" work happens in the background.

Requirements:

  • The Producer: Create a script register.php that simulates a user registration. It should accept a username and email, save them to a mock database (or just an array), and publish a message to a queue named user_welcome_queue containing the user's ID and email.
  • The Consumer: Create a script welcome_worker.php that listens to user_welcome_queue. For every message it receives, it must simulate three distinct tasks:
    1. Sending a "Welcome" email (sleep(2)).
    2. Creating a default user profile in a database (sleep(1)).
    3. Notifying the admin team via a Slack webhook (sleep(1)).
  • Reliability: Ensure the consumer uses manual acknowledgments so that if the script is killed during the "Welcome Email" phase, the task is not lost.

Testing your build: Run welcome_worker.php in one terminal window. Run register.php in another (or via browser) and observe how the registration completes immediately while the worker processes the tasks sequentially in the background.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.