Scheduling with RavenDB
I got a question from one of our users about how they can use RavenDB to manage scheduled tasks. Stuff like: “Send this email next Thursday” or “Cancel this reservation if the user didn’t pay within 30 minutes.”
As you can tell from the context, this is both more straightforward and more complex than the “run this every 2nd Wednesday" you’ll typically encounter when talking about scheduled jobs.
The answer for how to do that in RavenDB is pretty simple, you use the Document Refresh feature. This is a really tiny feature when you consider what it does. Given this document:
{
"Redacted": "Details",
"@metdata": {
"@collection": "RoomAvailabilities",
"@refresh": "2025-09-14T10:00:00.0000000Z"
}
}
RavenDB will remove the @refresh
metadata field at the specified time. That is all this does, nothing else. That looks like a pretty useless feature, I admit, but there is a point to it.
The act of removing the @refresh
field from the document will also (obviously) update the document, which means that everything that reacts to a document update will also react to this.
I wrote about this in the past, but it turns out there are a lot of interesting things you can do with this. For example, consider the following index definition:
from RoomAvailabilitiesas r
where true and not exists(r."@metadata"."@refresh")
select new {
r.RoomId,
r.Date,
// etc...
}
What you see here is an index that lets me “hide” documents (that were reserved) until that reservation expires.
I can do quite a lot with this feature. For example, use this in RabbitMQ ETL to build automatic delayed sending of documents. Let’s implement a “dead-man switch”, a document will be automatically sent to a RabbitMQ channel if a server doesn’t contact us often enough:
if (this['@metadata']["@refresh"])
return; // no need to send if refresh didn't expire
var alertData = {
Id: id(this),
ServerId: this.ServerId,
LastUpdate: this.Timestamp,
LastStatus: this.Status || 'ACTIVE'
};
loadToAlertExchange(alertData, 'alert.operations', {
Id: id(this),
Type: 'operations.alerts.missing_heartbeat',
Source: '/operations/server-down/no-heartbeat'
});
The idea is that whenever a server contacts us, we’ll update the @refresh
field to the maximum duration we are willing to miss updates from the server. If that time expires, RavenDB will remove the @refresh
field, and the RabbitMQ ETL script will send an alert to the RabbitMQ exchange. You’ll note that this is actually reacting to inaction, which is a surprisingly hard thing to actually do, usually.
You’ll notice that, like many things in RavenDB, most features tend to be small and focused. The idea is that they compose well together and let you build the behavior you need with a very low complexity threshold.
The common use case for @refresh
is when you use RavenDB Data Subscriptions to process documents. For example, you want to send an email in a week. This is done by writing an EmailToSend document with a @refresh
of a week from now and defining a subscription with the following query:
from EmailToSend as e
where true and not exists(e.'@metadata'.'@refresh')
In other words, we simply filter out those that have a @refresh
field, it’s that simple. Then, in your code, you can ignore the scheduling aspect entirely. Here is what this looks like:
var subscription = store.Subscriptions
.GetSubscriptionWorker<EmailToSend>("EmailToSendSubscription");
await subscription.Run(async batch =>
{
using var session = batch.OpenAsyncSession();
foreach (var item in batch.Items)
{
var email = item.Result;
await EmailProvider.SendEmailAsync(new EmailMessage
{
To = email.To,
Subject = email.Subject,
Body = email.Body,
From = "no-reply@example.com"
});
email.Status = "Sent";
email.SentAt = DateTime.UtcNow;
}
await session.SaveChangesAsync();
});
Note that nothing in this code handles scheduling. RavenDB is in charge of sending the documents to the subscription when the time expires.
Using @refresh
+ Subscriptions in this manner provides us with a number of interesting advantages:
- Missed Triggers: Handles missed schedules seamlessly, resuming on the next subscription run.
- Reliability: Automatically retries subscription processing on errors.
- Rescheduling: When
@refresh
expires, your subscription worker will get the document and can decide to act or reschedule a check by updating the@refresh
field again. - Robustness: You can rely on RavenDB to keep serving subscriptions even if nodes (both clients & servers) fail.
- Scaleout: You can use concurrent subscriptions to have multiple workers read from the same subscription.
You can take this approach really far, in terms of load, throughput, and complexity. The nice thing about this setup is that you don’t need to glue together cron, a message queue, and worker management. You can let RavenDB handle it all for you.
Comments
Comment preview
Join the conversation...