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.