Skip to main content

Cron Jobs

This guide shows how to use the Restate to schedule cron jobs.

What is a cron job?

A cron job is a scheduled task that runs periodically at a specified time or interval. It is often used for background tasks like cleanup or sending notifications.

Restate has no built-in functionality for cron jobs. But Restate's durable building blocks make it easy to implement a service that does this for us, and uses the guarantees Restate gives to make sure tasks get executed reliably.

Restate has many features that make it a good fit for implementing cron jobs:

  • Durable timers: Schedule tasks to run at a specific time in the future. Restate ensures execution.
  • Task resiliency: Restate ensures that tasks are retried until they succeed.
  • Task control: Cancel and inspect running jobs.
  • K/V state: We store the details of the cron jobs in Restate, so we can retrieve them later and query them from the outside.
  • FaaS support: Run your services on FaaS infrastructure, like AWS Lambda. Restate will scale your scheduler and tasks to zero while they sleep.
  • Scalability: Restate can handle many cron jobs running in parallel, and can scale horizontally to handle more load.
  • Observability: See the execution history of the cron jobs, and their status in the Restate UI.

Example

The example implements a cron service that you can copy over to your own project.

Usage:

  1. Send requests to CronJobInitiator.create() to start new jobs with standard cron expressions:
    {
    "cronExpression": "0 0 * * *", # E.g. run every day at midnight
    "service": "TaskService", # Schedule any Restate handler
    "method": "executeTask",
    "key": "taskId", # Optional, Virtual Object key
    "payload": "Hello midnight!"
    }
  2. Each job gets a unique ID and runs as a CronJob virtual object
  3. Jobs automatically reschedule themselves after each execution
GitHub
export const cronJobInitiator = restate.service({
name: "CronJobInitiator",
handlers: {
create: async (ctx: restate.Context, request: JobRequest) => {
// Create a new job ID and initiate the cron job object for that ID
// We can then address this job object by its ID
const jobId = ctx.rand.uuidv4();
const job = await ctx.objectClient(cronJob, jobId).initiate(request);
return `Job created with ID ${jobId} and next execution time ${job.next_execution_time}`;
},
},
});
export const cronJob = restate.object({
name: "CronJob",
handlers: {
initiate: async (ctx: restate.ObjectContext, request: JobRequest): Promise<JobInfo> => {
if (await ctx.get<JobInfo>(JOB_STATE)) {
throw new TerminalError("Job already exists for this ID.");
}
return await scheduleNextExecution(ctx, request);
},
execute: async (ctx: restate.ObjectContext) => {
const jobState = await ctx.get<JobInfo>(JOB_STATE);
if (!jobState) {
throw new TerminalError("Job not found.");
}
// execute the task
const { service, method, key, payload } = jobState.request;
if (payload) {
ctx.genericSend({
service,
method,
parameter: payload,
key,
inputSerde: serde.json,
});
} else {
ctx.genericSend({
service,
method,
parameter: undefined,
key,
inputSerde: serde.empty,
});
}
await scheduleNextExecution(ctx, jobState.request);
},
cancel: async (ctx: restate.ObjectContext) => {
// Cancel the next execution
const jobState = await ctx.get<JobInfo>(JOB_STATE);
if (jobState) {
ctx.cancel(jobState.next_execution_id);
}
// Clear the job state
ctx.clearAll();
},
getInfo: async (ctx: restate.ObjectSharedContext) => ctx.get<JobInfo>(JOB_STATE),
},
});
const scheduleNextExecution = async (
ctx: restate.ObjectContext,
request: JobRequest
): Promise<JobInfo> => {
// Parse cron expression
// Persist current date in Restate for deterministic replay
const currentDate = await ctx.date.now();
let interval;
try {
interval = CronExpressionParser.parse(request.cronExpression, { currentDate });
} catch (e) {
throw new TerminalError(`Invalid cron expression: ${(e as Error).message}`);
}
const next = interval.next().toDate();
const delay = next.getTime() - currentDate;
// Schedule next execution for this job
const thisJobId = ctx.key; // This got generated by the CronJobInitiator
const handle = ctx.objectSendClient(cronJob, thisJobId, { delay }).execute();
// Store the job information
const jobState = {
request,
next_execution_time: next.toString(),
next_execution_id: await handle.invocationId,
};
ctx.set<JobInfo>(JOB_STATE, jobState);
return jobState;
};
Example not available in your language?

This pattern is implementable with any of our SDKs. We are still working on translating all patterns to all SDK languages. If you need help with a specific language, please reach out to us via Discord or Slack.

Adapt to your use case

Note that this implementation is fully resilient, but you might need to make some adjustments to make this fit your use case:

  • Take into account time zones.
  • Adjust how you want to handle tasks that fail until the next task gets scheduled. With the current implementation, you would have concurrent executions of the same cron job (one retrying and the other starting up). If you want to cancel the failing task when a new one needs to start, you can do the following: at the beginning of the execute call, retrieve the next_execution_id from the job state and check if it is completed by attaching to it with a timeout set to 0. If it is not completed, cancel it and start the new iteration.

Running the example

1
Download the example

restate example typescript-patterns-use-cases && cd typescript-patterns-use-cases

2
Start the Restate Server

restate-server

3
Start the Service

npx tsx watch ./src/cron/task_service.ts

4
Register the services

restate deployments register localhost:9080

5
Send a request

For example, run executeTask every minute:

curl localhost:8080/CronJobInitiator/create --json '{
"cronExpression": "* * * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello new minute!"
}'

For example, or run executeTask at midnight:

curl localhost:8080/CronJobInitiator/create --json '{
"cronExpression": "0 0 * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello midnight!"
}'

You can also use the cron service to execute handlers on Virtual Objects, by specifying the Virtual Object key in the request.

You will get back a response with the job ID.

Using the job ID, you can then get information about the job:

curl localhost:8080/CronJob/myJobId/getInfo

Or cancel the job later:

curl localhost:8080/CronJob/myJobId/cancel

4
Check the scheduled tasks and state

In the UI, you can see how the tasks are scheduled, and how the state of the cron jobs is stored in Restate.

You can kill and restart any of the services or the Restate Server, and the scheduled tasks will still be there.