# `step.sleepUntil`

`step.sleepUntil()` pauses a run until a date and time you choose. Inngest resumes it at that time without keeping your function process running.

They are useful when:

- You need to send a reminder at a scheduled time.
- A deadline determines when the next step should run.
- An event provides a timestamp for later work.

## Use a date or a duration

Use `step.sleepUntil()` when you have a deadline or a timestamp in event data. Use [step.sleep](/docs-markdown/durable-execution/primitives/step-sleep) when you want to wait for a duration, such as 30 minutes from the current step. If you need to delay the start of the whole function, set the event's `ts` field instead.

Pass a `Date` or an ISO 8601 string with a time zone, such as `2026-10-01T16:30:00Z`.

## Send an appointment reminder

This function waits until the timestamp supplied by the event, then runs the reminder action. The event sender supplies `remindAt` as an ISO 8601 timestamp with a time zone.

```typescript {{ title: "TypeScript" }}
import { Inngest } from "inngest";

const inngest = new Inngest({ id: "reminders" });

export const sendScheduledReminder = inngest.createFunction(
  { id: "send-scheduled-reminder", triggers: { event: "app/reminder.scheduled" } },
  async ({ event, step }) => {
    const remindAt = new Date(event.data.remindAt);
    if (Number.isNaN(remindAt.getTime())) {
      throw new Error("Invalid reminder time");
    }

    await step.sleepUntil("wait-for-reminder", remindAt);

    await step.run("send-reminder", async () => {
      await fetch("https://api.example.com/reminders", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ reminderId: event.data.reminderId }),
      });
    });
  }
);
```

```python {{ title: "Python" }}
import datetime

import httpx
import inngest

inngest_client = inngest.Inngest(app_id="reminders")

@inngest_client.create_function(
    fn_id="send-scheduled-reminder",
    trigger=inngest.TriggerEvent(event="app/reminder.scheduled"),
)
async def send_scheduled_reminder(ctx: inngest.Context) -> None:
    try:
        remind_at = datetime.datetime.fromisoformat(
            str(ctx.event.data["remindAt"])
        )
    except ValueError as err:
        raise ValueError("Invalid reminder time") from err

    await ctx.step.sleep_until("wait-for-reminder", remind_at)

    async def send_reminder() -> None:
        async with httpx.AsyncClient() as http:
            await http.post(
                "https://api.example.com/reminders",
                json={"reminderId": ctx.event.data["reminderId"]},
            )

    await ctx.step.run("send-reminder", send_reminder)
```

```go {{ title: "Go", filename: 'main.go' }}
import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

type ReminderScheduled struct {
	ReminderID string `json:"reminderId"`
	RemindAt   string `json:"remindAt"`
}

func main() {
	client, err := inngestgo.NewClient(inngestgo.ClientOpts{AppID: "reminders"})
	if err != nil {
		panic(err)
	}

	_, err = inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "send-scheduled-reminder"},
		inngestgo.EventTrigger("app/reminder.scheduled", nil),
		func(ctx context.Context, input inngestgo.Input[ReminderScheduled]) (any, error) {
			remindAt, err := time.Parse(time.RFC3339, input.Event.Data.RemindAt)
			if err != nil {
				return nil, fmt.Errorf("invalid reminder time: %w", err)
			}

			step.SleepUntil(ctx, "wait-for-reminder", remindAt)

			return step.Run(ctx, "send-reminder", func(ctx context.Context) (any, error) {
				body, err := json.Marshal(map[string]string{
					"reminderId": input.Event.Data.ReminderID,
				})
				if err != nil {
					return nil, err
				}
				resp, err := http.Post(
					"https://api.example.com/reminders",
					"application/json",
					bytes.NewReader(body),
				)
				if err != nil {
					return nil, err
				}
				return nil, resp.Body.Close()
			})
		},
	)
	if err != nil {
		panic(err)
	}
}
```

Replace the example request with your reminder service. Check its response and make the action safe to retry before using it in production.

## What happens while the function waits

Inngest pauses the run and resumes it at the scheduled time. Your code uses no compute during the wait, and the sleeping run does not count against step concurrency. The wait survives server restarts and redeploys. When the run resumes, Inngest re-invokes the function and uses saved step state to continue past completed work. A later `step.run()` can retry without re-running earlier completed steps.

## Limits and pitfalls

- A sleep can last up to one year, but the whole run, including every sleep, must finish within your plan's [maximum function run length](/docs-markdown/durable-execution/limits#plan-based-limits): 30 days on Free, 90 days on Pro, 366 days on Business, and custom on Enterprise.
- Validate timestamps before calling `step.sleepUntil()`. Include an explicit time zone to avoid scheduling for an unintended local time.
- Use `await`. Omitting it can prevent the function from sleeping correctly.
- Keep the step ID stable when you change the function. Inngest uses it to match saved state across versions.
- A long sleep can outlast your plan's trace and log history window. The run still resumes, but it may no longer appear in the Function Runs view while it sleeps.
- Use an event wait when the next action depends on an event, rather than on a known time.

## Next

- [step.sleep](/docs-markdown/durable-execution/primitives/step-sleep) for a duration.
- [step.waitForEvent](/docs-markdown/durable-execution/primitives/step-waitforevent) for an external event.
- [Schedules and delayed starts](/docs-markdown/durable-execution/guides-and-advanced/events-and-triggers/schedules-and-delayed-starts) when the wait should happen before the function starts.
- [Limits](/docs-markdown/durable-execution/limits) for the plan's maximum wait and run length.