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.

How does Restate help?

Restate has many features that make it a good fit for implementing cron jobs:
  • Durable timers: Schedule tasks reliably for future execution.
  • Task resiliency: Automatic retries until success.
  • Task control: Inspect and cancel running jobs.
  • K/V state: Store and query cron job details using K/V storage.
  • FaaS support: Integrates with platforms like AWS Lambda, scaling to zero when idle.
  • Scalability: Handles many parallel jobs; scales horizontally.
  • Observability: View execution history and job status in the Restate UI.
Restate doesn’t have built-in cron functionality, but its building blocks make it easy to build reliable, custom schedulers.

Example

To implement cron jobs, you can copy over the following file (TS / Java / Go) to your project:
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: restate.handlers.object.shared(async (ctx: restate.ObjectSharedContext) => {
      return 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;
};
This contains two services: a CronJobInitiator Service, and a CronJob Virtual Object: Cron Jobs Diagram Bind both services to your endpoint, and register them. Usage:
  • 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!"
}
  • Each job gets a unique ID and runs as a CronJob Virtual Object.
  • Jobs automatically reschedule themselves after each execution.
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.

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

npm install
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
6

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.Cron Service ScheduleCron State UIYou can kill and restart any of the services or the Restate Server, and the scheduled tasks will still be there.

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.