RabbitMQ
C# API
Event-based Consumption
Message Queue
Programming

RabbitMQ C# API Event based Message Consumption

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In the RabbitMQ .NET client, event-based consumption means you register a consumer and handle messages when RabbitMQ pushes them to you. This model is usually simpler than writing your own polling loop, and it fits naturally with long-running worker processes that should react to messages as they arrive.

The Core Consumer Type

In C#, the usual starting point is EventingBasicConsumer. You create a connection, open a channel, attach a handler to the Received event, and call BasicConsume.

A minimal example looks like this:

csharp
1using System;
2using System.Text;
3using RabbitMQ.Client;
4using RabbitMQ.Client.Events;
5
6var factory = new ConnectionFactory { HostName = "localhost" };
7using var connection = factory.CreateConnection();
8using var channel = connection.CreateModel();
9
10channel.QueueDeclare(queue: "demo",
11                     durable: false,
12                     exclusive: false,
13                     autoDelete: false,
14                     arguments: null);
15
16var consumer = new EventingBasicConsumer(channel);
17consumer.Received += (sender, ea) =>
18{
19    var body = ea.Body.ToArray();
20    var message = Encoding.UTF8.GetString(body);
21    Console.WriteLine($"Received: {message}");
22};
23
24channel.BasicConsume(queue: "demo", autoAck: true, consumer: consumer);
25
26Console.WriteLine("Waiting for messages. Press Enter to exit.");
27Console.ReadLine();

This is event-driven because your code does not repeatedly ask the queue for work. RabbitMQ delivers messages to the consumer, and the event handler processes them.

Why Manual Acknowledgment Usually Matters

autoAck: true is fine for a demo, but it is risky in real systems. With auto-ack enabled, RabbitMQ marks the message handled as soon as it is delivered, even if your code crashes before finishing the work.

A safer pattern is manual acknowledgment:

csharp
1using System;
2using System.Text;
3using RabbitMQ.Client;
4using RabbitMQ.Client.Events;
5
6var factory = new ConnectionFactory { HostName = "localhost" };
7using var connection = factory.CreateConnection();
8using var channel = connection.CreateModel();
9
10channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
11
12var consumer = new EventingBasicConsumer(channel);
13consumer.Received += (sender, ea) =>
14{
15    try
16    {
17        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
18        Console.WriteLine($"Processing: {message}");
19
20        // Do work here.
21
22        channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
23    }
24    catch (Exception ex)
25    {
26        Console.WriteLine($"Failed: {ex.Message}");
27        channel.BasicNack(deliveryTag: ea.DeliveryTag, multiple: false, requeue: true);
28    }
29};
30
31channel.BasicConsume(queue: "demo", autoAck: false, consumer: consumer);
32Console.ReadLine();

Here the message is acknowledged only after successful processing.

Why BasicQos Helps

Without a prefetch limit, RabbitMQ can push many unacknowledged messages to one consumer. For workloads where processing takes time, setting prefetchCount prevents one worker from grabbing too much work at once.

This line is common:

csharp
channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

It tells RabbitMQ to send one unacknowledged message at a time to that consumer, which is often a good default for fair dispatch in worker-style systems.

Keep the Process Alive

Event-based consumption does not mean RabbitMQ runs your handler in some magical background universe after your program exits. The connection and channel must stay open, and the process must remain alive.

That is why console samples often end with:

csharp
Console.ReadLine();

In a real application, the consumer usually lives inside a hosted service, Windows service, or background worker that stays up for the lifetime of the process.

Think About Error Handling and Idempotency

A nack with requeue: true can lead to the same message being delivered again. That is often correct, but your processing logic should tolerate retries.

Good habits include:

  • making handlers idempotent where possible
  • logging delivery failures clearly
  • using dead-letter queues when repeated retries are not helpful
  • acknowledging only after the actual work succeeds

Event-based consumption is easy to wire up, but reliable consumption requires careful semantics around retry and acknowledgment.

Event-Based Does Not Mean Parallel by Default

Developers sometimes assume that because consumption is event-driven, processing is automatically massively parallel. That is not guaranteed.

Parallelism depends on:

  • how many consumer instances you run
  • how the channel is used
  • prefetch settings
  • whether your handler itself starts background work

The event model is about delivery style, not automatic scaling.

Common Pitfalls

The biggest mistake is using autoAck: true in code that can fail after receiving the message. That trades reliability for convenience.

Another issue is disposing the connection or channel too early. If the process exits or the channel closes, the consumer stops no matter how correct the event handler looks.

Developers also forget about BasicQos, which can lead to unbalanced work distribution and too many in-flight messages on one consumer.

Finally, do not assume requeueing is always safe. A poison message can loop forever unless you add dead-letter handling or a retry strategy.

Summary

  • Event-based RabbitMQ consumption in C# is commonly implemented with EventingBasicConsumer.
  • Register a Received handler and start consuming with BasicConsume.
  • Use manual acknowledgments for real workloads whenever reliability matters.
  • Keep the channel alive and set BasicQos deliberately.
  • Treat retries, poison messages, and idempotency as part of the consumer design, not afterthoughts.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.