step.sleep() & step.sleepUntil()
Pause a workflow for a duration or until a specific time
Overview
step.sleep() pauses a workflow for a specified duration using Durable Object alarms. No resources consumed while sleeping, no memory held, no charges for nap time.
await step.sleep('wait-before-retry', '30s');
await step.sleep('cool-down-period', '5m');
await step.sleep('wait-for-next-day', '24h');Sleeping workflows consume zero resources. The Durable Object is hibernated and only wakes up when the alarm fires. Sleep for a week? That's fine. Sleep for a month? Also fine. Cloudflare won't charge you for time spent dreaming.
Basic Usage
const workflow = defineWorkflow((t) => ({
type: 'scheduled-reminder',
input: t.object({ userId: t.string() }),
run: async (step, payload) => {
await step.do('send-immediate-email', async () => {
await sendEmail(payload.userId, 'Thanks for signing up!');
});
// Wait 7 days
await step.sleep('wait-one-week', '7d');
await step.do('send-follow-up-email', async () => {
await sendEmail(payload.userId, 'How are you liking the product?');
});
return { remindersSent: 2 };
},
}));The workflow sends an immediate email, sleeps for 7 days (consuming zero resources during that time), then sends a follow-up email.
Duration Formats
Duration strings support the following units:
| Unit | Example | Description |
|---|---|---|
ms | "500ms" | Milliseconds |
s | "30s" | Seconds |
m | "5m" | Minutes |
h | "24h" | Hours |
d | "7d" | Days |
await step.sleep('brief-pause', '500ms');
await step.sleep('half-minute', '30s');
await step.sleep('five-minutes', '5m');
await step.sleep('one-hour', '1h');
await step.sleep('one-week', '7d');Invalid duration strings throw InvalidDurationError:
// ❌ Invalid formats
await step.sleep('bad', '5'); // Missing unit
await step.sleep('bad', '5 seconds'); // Space not allowed
await step.sleep('bad', '5y'); // 'y' is not a valid unit
// ✅ Valid formats
await step.sleep('good', '5s');
await step.sleep('good', '500ms');step.sleepUntil()
When you need to sleep until a specific point in time rather than a relative duration, use step.sleepUntil(). It accepts a Date object instead of a duration string.
// Sleep until midnight UTC on January 15th
await step.sleepUntil('wait-for-midnight', new Date('2025-01-15T00:00:00Z'));
// Sleep until a computed time
const launchDate = new Date(payload.scheduledAt);
await step.sleepUntil('wait-for-launch', launchDate);When to Use sleepUntil vs sleep
| Use Case | Method |
|---|---|
| Wait a fixed amount of time | step.sleep('pause', '30s') |
| Wait until a specific date/time | step.sleepUntil('launch', date) |
| Schedule for a calendar event | step.sleepUntil('meeting', meetingTime) |
| Rate limiting between operations | step.sleep('throttle', '1s') |
Past Dates
If the date is in the past, the alarm fires immediately and execution continues without delay. This is useful for workflows where the target time may have already passed during prior step execution.
Invalid Dates
Passing an invalid Date (e.g., new Date('garbage')) throws InvalidDateError:
// ❌ Throws InvalidDateError
await step.sleepUntil('bad', new Date('not-a-date'));
// ✅ Valid Date objects
await step.sleepUntil('good', new Date('2025-06-15T09:00:00Z'));
await step.sleepUntil('computed', new Date(Date.now() + 3600_000));Example: Scheduled Task
const workflow = defineWorkflow((t) => ({
type: 'scheduled-report',
input: t.object({ reportDate: t.string() }),
run: async (step, payload) => {
const targetDate = new Date(payload.reportDate);
await step.sleepUntil('wait-for-report-date', targetDate);
const report = await step.do('generate-report', async () => {
return await generateMonthlyReport();
});
await step.do('send-report', async () => {
await sendEmail('team@company.com', report);
});
return { sent: true };
},
}));How It Works
Both step.sleep() and step.sleepUntil() use the same underlying mechanism:
First Execution
- Creates a "sleeping" step in SQLite with a
wakeAttimestamp - Throws a
SleepInterrupt(not an Error — this is normal flow control) - The workflow runner catches the interrupt
- The Durable Object sets an alarm for the
wakeAttime - The Durable Object hibernates
When the Alarm Fires
- The Durable Object wakes up
- The step is marked as "completed" in SQLite
- The workflow replays from the beginning
- All previous steps return cached results instantly
step.sleep()sees the step is completed and returns immediately- Execution continues past the sleep
Subsequent Replays
On any future replay (even years later), the completed sleep step returns instantly. The workflow doesn't sleep again.
Use Cases
Rate Limiting
Implement backoff between API calls:
for (let i = 0; i < items.length; i++) {
await step.do(`process-item-${i}`, async () => {
await processItem(items[i]);
});
if (i < items.length - 1) {
await step.sleep(`rate-limit-${i}`, '1s');
}
}Scheduled Reminders
Send time-delayed notifications:
const workflow = defineWorkflow((t) => ({
type: 'trial-reminder-sequence',
input: t.object({ userId: t.string() }),
run: async (step, { userId }) => {
await step.sleep('wait-3-days', '3d');
await step.do('send-3-day-reminder', async () => {
await sendEmail(userId, '3 days left in your trial!');
});
await step.sleep('wait-2-more-days', '2d');
await step.do('send-1-day-reminder', async () => {
await sendEmail(userId, 'Last day of your trial!');
});
await step.sleep('wait-final-day', '1d');
await step.do('convert-or-downgrade', async () => {
await handleTrialExpiration(userId);
});
},
}));Retry with Backoff
Implement custom retry logic:
let attempt = 0;
const maxAttempts = 5;
while (attempt < maxAttempts) {
try {
const result = await step.do(`attempt-${attempt}`, async () => {
const res = await fetch('https://flaky.api/data');
if (!res.ok) throw new Error('API error');
return res.json();
});
return result; // Success!
} catch (error) {
attempt++;
if (attempt < maxAttempts) {
// Exponential backoff: 2s, 4s, 8s, 16s
const delay = Math.pow(2, attempt);
await step.sleep(`backoff-${attempt}`, `${delay}s`);
}
}
}
throw new Error('All attempts failed');(Though for this use case, you'd probably just use step.do() with the built-in retries option!)
Step Name Uniqueness
Like all step primitives, sleep step names must be unique within a workflow run:
// ❌ This will throw DuplicateStepError
await step.sleep('pause', '5s');
await step.sleep('pause', '10s');
// ✅ This is fine
await step.sleep('first-pause', '5s');
await step.sleep('second-pause', '10s');Performance Notes
Sleeping workflows consume zero compute resources. The Durable Object is completely hibernated during the sleep period. You can have thousands of workflows sleeping for days or weeks without any resource concerns.
The only limit is Cloudflare's alarm scheduling limits:
- Maximum alarm time is approximately 1 year in the future
- Alarms are reliable and survive Durable Object migrations
For workflows that need to sleep longer than a year, chain multiple sleep steps together. (But seriously, maybe rethink your workflow design at that point.)