Message queues vs event streaming — RabbitMQ vs Kafka context
Advanced5 min read·eng-11-010
interviewcompare
Concept
Message queues vs event streaming — two different paradigms for async communication between services.
Message Queue (RabbitMQ, SQS, Laravel Queue):
- Consumer pops the message: Once consumed, it's gone from the queue.
- Point-to-point or pub/sub: One consumer gets each message (work queue) OR all subscribers get a copy (fan-out/exchange).
- Designed for tasks: "Do this work exactly once." The consumer acknowledges the message — if it crashes before ACK, the broker re-delivers.
- Retention: Messages are stored only until consumed. No historical replay.
- Use case: Job queues, notifications, email sending, order processing. "I need this work done."
Event Streaming (Apache Kafka, AWS Kinesis):
- Consumers read events from a log: Events are NOT removed after consumption. They're appended to a persistent log.
- Multiple independent consumers: Each consumer has its own offset (position). Consumer A is at event 1000; Consumer B is at event 5000. Both read the same events independently.
- Log retention: Events retained for days/weeks (configurable). New services can replay all historical events from the beginning.
- High throughput: Kafka handles millions of events per second. Sequential disk writes are fast.
- Use case: Audit logs, event sourcing, multiple services consuming the same event, analytics pipelines, replaying history.
Key difference: In a message queue, a message is consumed and gone. In event streaming, the event is persistent — many consumers read it independently, and new consumers can read from the past.
PHP connection: Laravel's queue system supports database, Redis, SQS (message queues). For Kafka, use php-rdkafka or enqueue/rdkafka. Or produce Kafka events via an HTTP sidecar.
When to choose:
- Tasks with exactly-once processing → queue.
- Multiple consumers, historical replay, audit trail → event streaming.
- Most PHP apps → queue (simpler, already in Laravel).
Code Example
php
<?php
// ============================================================
// MESSAGE QUEUE — Laravel / Redis (work queue pattern)
// ============================================================
// Producer — order service dispatches a job
class OrderController extends Controller
{
public function store(): \Illuminate\Http\JsonResponse
{
$order = Order::create([...]);
SendOrderConfirmationEmail::dispatch($order); // "do this work once"
return response()->json($order, 201);
}
}
// One consumer picks this up and sends the email
// If it fails: retried up to $tries times, then → failed_jobs
// Other jobs in the queue are not affected
// Fan-out with multiple queues (simulated pub/sub in Laravel)
class OrderPlacedEventJob
{
public function handle(): void
{
// Dispatch to multiple specific queues
SendOrderEmail::dispatch($this->order)->onQueue('notifications');
UpdateInventory::dispatch($this->order)->onQueue('inventory');
NotifyAnalytics::dispatch($this->order)->onQueue('analytics');
}
}
// ============================================================
// EVENT STREAMING — Kafka conceptual PHP example
// ============================================================
// Using php-rdkafka (not Laravel-native, but the concept)
// Producer — append event to Kafka topic
$producer = new \RdKafka\Producer();
$producer->addBrokers('kafka:9092');
$topic = $producer->newTopic('order-events');
$topic->produce(RD_KAFKA_PARTITION_UA, 0, json_encode([
'type' => 'OrderPlaced',
'order_id' => $order->id,
'at' => now()->toIso8601String(),
'data' => $order->toArray(),
]));
$producer->flush(1000);
// Consumer A — inventory service, starts at its own offset
$consumer = new \RdKafka\KafkaConsumer($conf);
$consumer->subscribe(['order-events']);
while (true) {
$message = $consumer->consume(10_000);
$event = json_decode($message->payload, true);
if ($event['type'] === 'OrderPlaced') {
decrementStock($event['data']['items']);
}
}
// Consumer B — analytics service, reads the SAME events independently
// Both consumers read from the same Kafka topic, each maintaining their own offset
// Consumer A at offset 5000, Consumer B at offset 4800 — both reading independently
// NEW: replay all events from beginning for a new analytics service
$consumer->assign([new \RdKafka\TopicPartition('order-events', 0, RD_KAFKA_OFFSET_BEGINNING)]);
// Processes ALL historical events — impossible with a message queue (they're consumed and gone)