Ablauf
Steps

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:

UnitExampleDescription
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 CaseMethod
Wait a fixed amount of timestep.sleep('pause', '30s')
Wait until a specific date/timestep.sleepUntil('launch', date)
Schedule for a calendar eventstep.sleepUntil('meeting', meetingTime)
Rate limiting between operationsstep.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

  1. Creates a "sleeping" step in SQLite with a wakeAt timestamp
  2. Throws a SleepInterrupt (not an Error — this is normal flow control)
  3. The workflow runner catches the interrupt
  4. The Durable Object sets an alarm for the wakeAt time
  5. The Durable Object hibernates

When the Alarm Fires

  1. The Durable Object wakes up
  2. The step is marked as "completed" in SQLite
  3. The workflow replays from the beginning
  4. All previous steps return cached results instantly
  5. step.sleep() sees the step is completed and returns immediately
  6. 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.)

On this page