Skip to content
Effect Days 2026 Get your ticket
Docs menu / Choosing and combining schedules

Choosing and combining schedules

Start with the schedule that defines when the next recurrence can happen. Add a function that limits or adjusts that schedule when one timing rule is enough. Combine schedules when multiple timing rules must work together or run in sequence.

This page assumes you already know whether the action should use Effect.repeat, Effect.schedule, or Effect.retry.

Choose a starting rule

Choose the constructor from the timing requirement:

RequirementConstructorBehavior
Limit immediate recurrencesSchedule.recurs(2)Allows at most two recurrences without adding a delay
Wait after each actionSchedule.spaced("5 minutes")Starts the full delay after the previous action finishes
Follow a regular cadenceSchedule.fixed("5 minutes")Keeps starts aligned to five-minute intervals
Increase the delaySchedule.exponential("250 millis")Increases the delay after each recurrence
Run at calendar timesSchedule.cron("0 3 * * *", "UTC")Uses a cron expression and time zone

See Scheduling work with cron for cron expressions, time zones, and validation.

How the starting rules advance

The diagrams below show the recurrences that each schedule permits after an initial action has run.

Schedule.recurs

Schedule.recurs(2) permits two more runs without adding a delay.

Schedule.recurs(2)

Schedule.spaced

Schedule.spaced("5 minutes") starts the full delay when the previous action finishes. If each action takes two minutes, starts are seven minutes apart.

Schedule.spaced(5 minutes)

Schedule.fixed

Schedule.fixed("5 minutes") keeps starts aligned to five-minute intervals. The same two-minute action starts at 0, 5, and 10 minutes.

Schedule.fixed(5 minutes)

If an action takes longer than the interval, the next recurrence happens immediately. Schedule.fixed does not replay the intervals that were missed.

Schedule.exponential

With the default factor of two, Schedule.exponential("250 millis") doubles the delay after every recurrence.

Schedule.exponential(250 millis)

Schedule.cron

Schedule.cron("0 3 * * *", "UTC") permits a recurrence at 03:00 UTC each day.

Schedule.cron at 03:00 UTC

Limit or adjust one schedule

Several functions take an existing schedule and return a new one with an added rule:

RequirementFunctionResult
Limit recurrences or elapsed timeSchedule.upToStops the schedule when a limit is reached
Stop from schedule metadataSchedule.whileContinues only while a condition holds
Change the selected delaySchedule.modifyDelayReplaces the delay for each recurrence
Randomize delaysSchedule.jitteredAdds jitter to reduce synchronized retries
Observe schedule decisionsSchedule.tapRuns an Effect without changing the schedule

Use these functions when one schedule already provides the timing rule you need.

Example (Limiting an Exponential Backoff)

Schedule.upTo can limit an exponential backoff by number of recurrences, elapsed duration, or both:

import { Schedule } from "effect"
const boundedBackoff = Schedule.exponential("250 millis").pipe(
Schedule.upTo({ times: 5, duration: "30 seconds" }),
)

The schedule stops as soon as either limit is reached. times: 5 allows five schedule recurrences. With Effect.repeat or Effect.retry, the action also runs once before the schedule is stepped.

Combine schedules only when the requirement depends on timing rules from separate schedules.

Require every rule with Schedule.max

Schedule.max continues only while every schedule can recur. For each recurrence, it uses the largest delay selected by the schedules.

Example (Combining Backoff with Minimum Spacing)

Suppose an exponential backoff controls how retry delays grow, while an API client requires at least one second between requests. Schedule.max applies both rules by selecting the larger delay. Schedule.upTo adds the retry limit separately.

import { Schedule } from "effect"
const backoff = Schedule.exponential("250 millis")
const minimumSpacing = Schedule.spaced("1 second")
const retryPolicy = Schedule.max([backoff, minimumSpacing]).pipe(
Schedule.upTo({ times: 5 }),
)

For each recurrence, both schedules propose a delay and Schedule.max selects the larger one:

RecurrenceExponential backoffMinimum spacingDelay selected by max
1250 ms1 s1 s
2500 ms1 s1 s
31 s1 s1 s
42 s1 s2 s
54 s1 s4 s

The minimum spacing determines the first three delays. Starting with the fourth recurrence, the exponential backoff is larger.

Allow any rule with Schedule.min

Schedule.min continues while at least one schedule can recur. It uses the smallest delay among the schedules that are still active.

Example (Adding Faster Startup Probes)

Suppose an application needs two quick startup probes while its regular health checks stay aligned to a ten-second cadence. Both schedules start together. Schedule.min lets the startup policy select the shorter delays while it is active.

import { Schedule } from "effect"
const startupProbes = Schedule.spaced("1 second").pipe(
Schedule.upTo({ times: 2 }),
)
const healthChecks = Schedule.fixed("10 seconds")
const combinedChecks = Schedule.min([startupProbes, healthChecks])

For each recurrence, Schedule.min selects the shortest delay proposed by an active schedule:

RecurrenceElapsed timeStartup probesHealth-check cadenceDelay selected by min
10 s1 s10 s1 s
21 s1 s9 s1 s
32 sComplete8 s8 s
410 sComplete10 s10 s

The startup policy controls the first two delays. After it completes, the health-check schedule continues on its original ten-second cadence. Schedule.min stops only when every input schedule has completed.

Run rules in phases with Schedule.concat

Use Schedule.concat when one schedule must finish before another starts.

Example (Reconnecting in Two Stages)

This reconnect policy tries three short waits, then switches to as many as ten long waits:

import { Schedule } from "effect"
const quickRetries = Schedule.spaced("100 millis").pipe(
Schedule.upTo({ times: 3 }),
)
const slowRetries = Schedule.spaced("5 seconds").pipe(
Schedule.upTo({ times: 10 }),
)
const reconnect = quickRetries.pipe(Schedule.concat(slowRetries))

concat merges the output types of both phases. Use Schedule.concatResult when later code must distinguish which phase produced an output.

See Retry quickly, then slow down for a complete retrying program. The Schedule cookbook also covers server-provided delays, hard deadlines, and retry observability. The Schedule API reference lists every available function.