Design Patterns & Practices  

Using Rebus Deferred Messages for Background Processing in .NET

Introduction

In many applications, we need to run background work at a specific time in the future, generating reports, sending reminders, cleaning up data, etc.

Let’s say we have a use case where we want to notify a downstream service after some time based on the actions the users performed earlier. For example, a user has signed up but hasn’t performed KYC yet and you want to remind them after a certain period. Or a user forgot to update the status of their work item and you want to send a gentle nudge.

The traditional approach is often a polling job (e.g., a BackgroundService or Hangfire recurring job) that wakes up regularly and checks for work. While simple, this creates unnecessary load and makes the system harder to debug.

Rebus offers a much more elegant solution: deferred messages. You publish an event or command now, and Rebus delivers it exactly when you want.

In this article, I’ll show you how I replaced polling with deferred events using Rebus, the benefits, the setup, and important gotchas.

Architecture Diagram

Screenshot_19-7-2026_20629_

Why Deferred Messages Beat Polling

  • Efficiency: No constant database or queue polling.

  • Precision: Messages are delivered at the exact time you specify.

  • Observability: Everything flows through your messaging infrastructure — easier tracing.

  • Scalability: Naturally fits event-driven architecture.

  • Reliability: Leverages Rebus retries, outbox, and transport features.

Prerequisites

  • Basic understanding of Rebus.

  • A transport (RabbitMQ, Azure Service Bus, Azure Storage Queues, etc.).

  • Rebus NuGet packages.

Setting Up Rebus (Quick Recap)

services.AddRebus(configure => configure
    .Transport(t => t.UseRabbitMq("amqp://...", "your-input-queue"))
    .Routing(r => r.TypeBased().MapAssemblyOf<YourMessage>())
    // Optional but recommended
    .Timeouts(t => t.UseSqlServer(connectionString, "RebusTimeouts")) // or StoreInMemory
    .Outbox(o => o.UseSqlServer(connectionString, "RebusOutbox"))
);

Publishing a Deferred Message

This is the core feature:

public class ReminderService
{
    private readonly IBus _bus;

    public ReminderService(IBus bus)
    {
        _bus = bus;
    }

    public async Task ScheduleReminderAsync(Guid userId, string reminderType, TimeSpan remindAfter)
    {
        var command = new SendReminderCommand
        {
            UserId = userId,
            ReminderType = reminderType, // e.g., "KYC", "StatusUpdate"
            OriginalActionDate = DateTimeOffset.UtcNow
        };

        // Defer the message until the desired time
        await _bus.Defer(remindAfter, command);
    }
}

The Command and Handler

public record SendReminderCommand
{
    public Guid UserId { get; init; }
    public string ReminderType { get; init; }
    public DateTimeOffset OriginalActionDate { get; init; }
}

public class SendReminderCommandHandler : IHandleMessages<SendReminderCommand>
{
    private readonly INotificationService _notificationService;
    private readonly IBus _bus;

    public SendReminderCommandHandler(INotificationService notificationService, IBus bus)
    {
        _notificationService = notificationService;
        _bus = bus;
    }

    public async Task Handle(SendReminderCommand message)
    {
        try
        {
            await _notificationService.SendReminderAsync(
                message.UserId, 
                message.ReminderType);

            // Optionally publish a completion event
            await _bus.Publish(new ReminderSentEvent
            {
                UserId = message.UserId,
                ReminderType = message.ReminderType,
                SentAt = DateTimeOffset.UtcNow
            });
            
            // OR schedule another reminder for the next timeout
        }
        catch (Exception ex)
        {
            // Rebus will retry based on your configuration
            throw;
        }
    }
}

Example Usage in Controller

[HttpPost("signup")]
public async Task<IActionResult> Signup(...)
{
    // After successful signup
    await _reminderService.ScheduleReminderAsync(
        userId: newUser.Id,
        reminderType: "KYC",
        remindAfter: TimeSpan.FromDays(7));

    return Ok();
}

Key Considerations

  • Using TimeSpan is often simpler for relative delays (e.g., “7 days from now”, “48 hours after last activity”). Use DateTimeOffset when you need an exact calendar time (e.g., “remind on Monday at 9 AM”).

  • Make handlers idempotent.

  • Monitor your timeout storage as the number of deferred messages grows.

  • Combine with Rebus Sagas if you need multi-step reminder workflows (e.g., escalate after 2nd reminder).

Conclusion

Switching from polling jobs to Rebus deferred messages for user reminders has made my background processing much cleaner and more maintainable. It fits naturally into event-driven architecture and reduces operational noise significantly.

Have you used deferred messaging for reminders or notifications? What patterns work best in your applications?

I’d love to hear your thoughts in the comments.