Schedules and delayed starts
Schedule recurring or future work without keeping an app timer running.
Run a daily report or send a reminder at a chosen time without keeping an app timer running. Inngest stores the schedule or delay and starts the function when it is due. This works on any provider or platform, including serverless hosts, and survives server restarts and redeploys. You never manage a queue or backlog.
Run on a recurring schedule
Use a cron trigger for work such as a daily report.
import { cron } from "inngest";
// Assumes `inngest` is your client and `buildReport` is defined elsewhere.
export const dailyReport = inngest.createFunction(
{
id: "daily-report",
triggers: [cron("0 9 * * *")],
},
async ({ step }) => {
return step.run("build-report", buildReport);
}
);
The basic cron expression runs at 09:00 UTC. A cron expression has five fields: minute, hour, day of month, month, and day of week. For example, 0 9 * * * runs at 09:00 every day, 0 * * * * runs at the start of every hour, and 0 12 * * 5 runs at 12:00 every Friday.
Your functions must be served so Inngest can find and call them.
Run in a local timezone
Prefix the expression with a timezone when the business schedule follows local time, for example TZ=Europe/Paris 0 9 * * *. Without a prefix, the schedule uses UTC.
Check daylight saving changes for schedules near clock transitions. Depending on the timezone and the schedule, a cron may run zero, one, or two times on the day clocks change. Inngest follows the underlying cron library and does not correct for daylight saving time.
To reduce the risk, avoid schedules in the transition hour, such as 2:00 AM in many US regions or 12:00 AM in some other regions. Use TZ=UTC when you need consistent timing.
Spread out runs with jitter
A cron function fires at the exact scheduled time by default. When many cron functions share a schedule, they all fire at once. That can create load spikes on your system or on third-party APIs.
Add a jitter to spread them out. Each occurrence then fires at a random time within the jitter window after the scheduled time. Jitter must be between 1 second and 5 minutes.
inngest.createFunction(
{
id: "hourly-sync",
triggers: [{ cron: "0 * * * *", jitter: "5m" }],
},
async ({ step }) => {
// Fires at a random time within 5 minutes after each hour
}
);
Fan out large scheduled jobs
A scheduled job that loops over many records can take a long time. Instead, use the cron function to send one event per record, then handle each record in a separate event-triggered function. These runs can execute in parallel. This is the fan-out pattern.
This weekly digest runs at 12:00 every Friday in the Paris timezone:
See createFunction() for all function options.
import { Inngest, cron } from "inngest";
const inngest = new Inngest({ id: "signup-flow" });
// Assumes `db` and `emailClient` are your own database and email helpers.
// This weekly digest function will run at 12:00pm on Friday in the Paris timezone
export const prepareWeeklyDigest = inngest.createFunction(
{ id: "prepare-weekly-digest", triggers: [cron("TZ=Europe/Paris 0 12 * * 5")] },
async ({ step }) => {
// Load all the users from your database:
const users = await step.run(
"load-users",
async () => await db.load("SELECT * FROM users")
);
// 💡 Since we want to send a weekly digest to each one of these users
// it may take a long time to iterate through each user and send an email.
// Instead, we'll use this scheduled function to send an event to Inngest
// for each user then handle the actual sending of the email in a separate
// function triggered by that event.
// ✨ This is known as a "fan-out" pattern ✨
// 1️⃣ First, we'll create an event object for every user return in the query:
const events = users.map((user) => {
return {
name: "app/send.weekly.digest",
data: {
user_id: user.id,
email: user.email,
},
};
});
// 2️⃣ Now, we'll send all events in a single batch:
await step.sendEvent("send-digest-events", events);
// This function can now quickly finish and the rest of the logic will
// be handled in the function below ⬇️
}
);
// This is a regular Inngest function that will send the actual email for
// every event that is received (see the above function's step.sendEvent())
// Since we are "fanning out" with events, these functions can all run in parallel
export const sendWeeklyDigest = inngest.createFunction(
{ id: "send-weekly-digest-email", triggers: { event: "app/send.weekly.digest" } },
async ({ event }) => {
// 3️⃣ We can now grab the email and user id from the event payload
const { email, user_id } = event.data;
// 4️⃣ Finally, we send the email itself:
await emailClient.send("weekly_digest", email, user_id);
// 🎇 That's it! - We've used two functions to reliably perform a scheduled
// task for a large list of users!
}
);
Handle repeated failures
On the free plan, Inngest pauses a function automatically after it fails 20 times in a row. Check failing cron functions so a scheduled job does not stop without notice.
Start once in the future
Set an event's ts to a future Unix millisecond timestamp when nothing must happen before the start time.
await inngest.send({
name: "notifications/reminder.due",
data: { reminderId: "rem_123" },
ts: Date.now() + 5 * 60 * 1000,
});
The event starts matching functions at that time. A future ts does not delay a waiting run that matches the event.
The function runs normally once it starts; the delay happens before the run, not inside it. Use this when you know the start time when you send the event. For a full walkthrough, see Scheduling a one-off function.
Pause an existing run
Use step.sleep() for a duration or step.sleepUntil() for a date when the run needs to do work before and after the pause. Inngest resumes the durable run after the wait; your host does not need to keep a process open.
Your function controls its own timing, so the logic stays in one place. When a run reaches a sleep, it stops and tells Inngest when to call it again. Inngest then calls the function at the next step and skips completed work. This is how a sleep outlasts serverless timeouts, server restarts, and redeploys.
Sleep for a duration
Use step.sleep():
import { Inngest } from "inngest";
const inngest = new Inngest({ id: "signup-flow" });
export const fn = inngest.createFunction(
{ id: "send-signup-email", triggers: { event: "app/user.created" } },
async ({ event, step }) => {
await step.sleep("wait-a-moment", "1 hour");
await step.run("do-some-work-in-the-future", async () => {
// This runs after 1 hour
});
}
);
Sleep until a date
You can pass an ISO date string or a timestamp from the event data, so the sender chooses when the run continues.
Use step.sleepUntil():
import { Inngest } from "inngest";
const inngest = new Inngest({ id: "signup-flow" });
export const fn = inngest.createFunction(
{ id: "send-signup-email", triggers: { event: "app/user.created" } },
async ({ event, step }) => {
await step.sleepUntil("wait-for-iso-string", "2023-04-01T12:30:00");
// You can also sleep until a timestamp within the event data. This lets you
// pass in a time for you to run the job:
await step.sleepUntil("wait-for-timestamp", event.data.run_at); // Assuming event.data.run_at is a timestamp.
await step.run("do-some-work-in-the-future", async () => {
// This runs at the specified time.
});
}
);
See step.sleep() and step.sleepUntil() for more detail.
Choose one
- Cron: repeat on a calendar schedule.
- Future event
ts: start new work once at a known time. - Sleep step: continue the same run later.
Check Limits for current delay allowances.