Documentation Index
Fetch the complete documentation index at: https://mintlify.com/effect-TS/effect-smol/llms.txt
Use this file to discover all available pages before exploring further.
The Schedule module provides utilities for creating and composing schedules for retrying operations, repeating effects, and implementing various timing strategies.
A Schedule is a function that takes an input and returns a decision whether to continue or halt, along with a delay duration. Schedules can be combined, transformed, and used to implement sophisticated retry and repetition logic.
Overview
Schedules are used with:
Effect.retry - Retry failed operations
Effect.repeat - Repeat successful operations
Effect.schedule - Schedule operations at intervals
import { Effect, Schedule } from "effect"
// Retry with exponential backoff
const retryPolicy = Schedule.exponential("100 millis", 2.0)
.pipe(Schedule.compose(Schedule.recurs(3)))
const program = Effect.gen(function*() {
const result = yield* Effect.retry(
Effect.fail("Network error"),
retryPolicy
)
})
// Repeat on a fixed schedule
const heartbeat = Effect.log("heartbeat")
.pipe(Effect.repeat(Schedule.spaced("30 seconds")))
Basic Schedules
Fixed Interval
Recur at fixed intervals.
import { Effect, Schedule } from "effect"
// Every 5 seconds
const every5Seconds = Schedule.spaced("5 seconds")
Effect.repeat(
Effect.log("Tick"),
every5Seconds
)
Limited Recurrence
Recur a specific number of times.
import { Schedule } from "effect"
// Retry up to 5 times
const maxRetries = Schedule.recurs(5)
// Retry exactly once
const retryOnce = Schedule.once
Exponential Backoff
Increase delay exponentially.
import { Schedule } from "effect"
// Start at 100ms, double each time
const exponential = Schedule.exponential("100 millis", 2.0)
// 100ms, 200ms, 400ms, 800ms, ...
// Start at 1 second, triple each time
const aggressive = Schedule.exponential("1 second", 3.0)
// 1s, 3s, 9s, 27s, ...
Fibonacci Backoff
Increase delay following Fibonacci sequence.
import { Schedule } from "effect"
const fibonacci = Schedule.fibonacci("100 millis")
// 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, ...
Duration-Based
Recur once after a specific duration.
import { Schedule } from "effect"
const afterDelay = Schedule.duration("5 seconds")
// Recurs once after 5 seconds, then completes
Combining Schedules
Both (Intersection)
Continue only while both schedules want to continue.
import { Schedule } from "effect"
// Retry with exponential backoff, but max 5 times
const limitedBackoff = Schedule.both(
Schedule.exponential("100 millis"),
Schedule.recurs(5)
)
// Use maximum delay, stop when first exhausts
const conservative = Schedule.both(
Schedule.spaced("1 second"),
Schedule.recurs(10)
)
Either (Union)
Continue while either schedule wants to continue.
import { Schedule } from "effect"
// Continue for 1 minute OR 20 attempts, whichever is longer
const flexible = Schedule.either(
Schedule.spaced("3 seconds"),
Schedule.recurs(20)
)
Sequential Composition
Run schedules in sequence.
import { Schedule } from "effect"
// Fast retries first, then slow retries
const phased = Schedule.andThen(
Schedule.exponential("100 millis").pipe(Schedule.take(3)),
Schedule.spaced("5 seconds").pipe(Schedule.take(5))
)
Limiting Duration
import { Schedule } from "effect"
// Retry for up to 30 seconds total
const timeBound = Schedule.exponential("100 millis")
.pipe(Schedule.upTo("30 seconds"))
// Retry while under 1 minute elapsed
const whileUnder = Schedule.spaced("5 seconds")
.pipe(Schedule.whileInput(({ elapsed }) => elapsed < 60000))
Limiting Attempts
import { Schedule } from "effect"
// Take only first 5 recurrences
const limited = Schedule.exponential("100 millis")
.pipe(Schedule.take(5))
Adding Jitter
Add randomness to delays.
import { Schedule } from "effect"
// Add random jitter (0-100% of original delay)
const jittered = Schedule.exponential("100 millis")
.pipe(Schedule.jittered)
// Custom jitter factor (0.0 to 1.0)
const customJitter = Schedule.exponential("100 millis")
.pipe(Schedule.jittered(0.5)) // ±50% jitter
Conditional Schedules
import { Effect, Schedule } from "effect"
// Only retry certain errors
const retryableOnly = Schedule.exponential("200 millis")
.pipe(
Schedule.setInputType<{ retryable: boolean }>(),
Schedule.while(({ input }) => input.retryable)
)
// Retry until success or max attempts
const whileError = Schedule.exponential("100 millis")
.pipe(
Schedule.whileInput((error) => error.status >= 500)
)
Modifying Delays
Adding Delays
import { Duration, Effect, Schedule } from "effect"
// Add fixed delay to each recurrence
const withExtraDelay = Schedule.exponential("100 millis")
.pipe(
Schedule.addDelay(() => Effect.succeed(Duration.millis(50)))
)
// Add random jitter
const withJitter = Schedule.exponential("100 millis")
.pipe(
Schedule.addDelay(() =>
Effect.succeed(Duration.millis(Math.random() * 100))
)
)
Modifying Delays
import { Duration, Effect, Schedule } from "effect"
// Cap maximum delay
const capped = Schedule.exponential("100 millis")
.pipe(
Schedule.modifyDelay((_, delay) =>
Effect.succeed(Duration.min(delay, Duration.seconds(10)))
)
)
Collecting Outputs
Collect All Outputs
import { Effect, Schedule } from "effect"
const collectAll = Schedule.collectOutputs(
Schedule.recurs(5)
)
const results = Effect.repeat(
Effect.succeed(Math.random()),
collectAll
)
// Returns array of all outputs: [0, 1, 2, 3, 4, 5]
import { Effect, Schedule } from "effect"
const collectInputs = Schedule.collectInputs(
Schedule.spaced("1 second")
)
let counter = 0
const program = Effect.repeat(
Effect.sync(() => `result-${++counter}`),
collectInputs.pipe(Schedule.take(3))
)
// Returns array of all inputs
Conditional Collection
import { Effect, Schedule } from "effect"
// Collect while under time limit
const timedCollection = Schedule.collectWhile(
Schedule.spaced("500 millis"),
(metadata) => Effect.succeed(metadata.elapsed < 3000)
)
Cron Schedules
Schedule based on cron expressions.
import { Effect, Schedule } from "effect"
// Every minute
const everyMinute = Schedule.cron("* * * * *")
// Every day at 2:30 AM
const dailyBackup = Schedule.cron("30 2 * * *")
// Every Monday at 9 AM (with timezone)
const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York")
// Every 15 minutes during business hours
const businessHours = Schedule.cron("0,15,30,45 9-17 * * 1-5")
Effect.repeat(
Effect.log("Scheduled task"),
everyMinute
)
Tapping and Effects
Tap Output
Perform side effects on outputs.
import { Console, Effect, Schedule } from "effect"
const logged = Schedule.exponential("100 millis")
.pipe(
Schedule.tapOutput((delay) =>
Console.log(`Next retry in ${delay}`)
)
)
Perform side effects on inputs.
import { Console, Effect, Schedule } from "effect"
const logErrors = Schedule.exponential("200 millis")
.pipe(
Schedule.setInputType<Error>(),
Schedule.tapInput((error) =>
Console.log(`Retrying after error: ${error.message}`)
)
)
Production Patterns
Capped Exponential with Jitter
import { Schedule } from "effect"
// Production-ready retry schedule
const productionRetry = Schedule.exponential("250 millis")
.pipe(
// Cap at 10 seconds
Schedule.either(Schedule.spaced("10 seconds")),
// Add jitter
Schedule.jittered,
// Max 10 attempts
Schedule.compose(Schedule.recurs(10))
)
Retry with Conditional Logic
import { Schedule } from "effect"
interface HttpError {
status: number
retryable: boolean
}
const smartRetry = Schedule.exponential("250 millis")
.pipe(
Schedule.either(Schedule.spaced("10 seconds")),
Schedule.jittered,
Schedule.setInputType<HttpError>(),
Schedule.while(({ input }) => input.retryable),
Schedule.compose(Schedule.recurs(6))
)
Phased Retry Strategy
import { Schedule } from "effect"
// Quick retries, then slow retries
const phased = Schedule.andThen(
// Fast phase: 3 quick retries
Schedule.exponential("100 millis").pipe(Schedule.take(3)),
// Slow phase: 3 slower retries
Schedule.exponential("2 seconds").pipe(Schedule.take(3))
)
Using Schedules
With Effect.retry
import { Data, Effect, Schedule } from "effect"
class ApiError extends Data.TaggedError("ApiError")<{
message: string
status: number
}> {}
const fetchData = Effect.gen(function*() {
// Simulated API call that might fail
if (Math.random() > 0.7) {
return { data: "success" }
}
return yield* new ApiError({ message: "Network error", status: 500 })
})
const withRetry = fetchData.pipe(
Effect.retry(Schedule.exponential("100 millis").pipe(
Schedule.compose(Schedule.recurs(5))
))
)
With Effect.repeat
import { Console, Effect, Schedule } from "effect"
const task = Console.log("Heartbeat")
// Repeat every 30 seconds
const repeated = Effect.repeat(
task,
Schedule.spaced("30 seconds")
)
// Repeat 10 times with delay
const limited = Effect.repeat(
task,
Schedule.spaced("5 seconds").pipe(Schedule.take(10))
)
Schedule Builder Helper
import { Effect, Schedule } from "effect"
interface MyError {
retryable: boolean
}
const effect = Effect.fail({ retryable: true })
// Type-safe schedule builder
const result = effect.pipe(
Effect.retry(($) =>
$(Schedule.spaced("1 seconds")).pipe(
Schedule.while(({ input }) => input.retryable)
)
)
)
import { Effect, Schedule } from "effect"
const metadataAware = Schedule.spaced("1 second")
.pipe(
Schedule.collectWhile((metadata) =>
Effect.succeed(
metadata.attempt <= 5 &&
metadata.elapsed < 10000
)
)
)
Advanced: Custom Schedules
Using unfold
import { Effect, Schedule } from "effect"
// Custom schedule that increases delay
const custom = Schedule.unfold(100, (delay) =>
Effect.succeed(delay * 1.5)
)
Using fromStep
import { Duration, Effect, Schedule } from "effect"
// Advanced: create schedule from step function
const advanced = Schedule.fromStep(
Effect.sync(() => {
let count = 0
return (now, input) => {
count++
const delay = Duration.millis(count * 100)
return Effect.succeed([count, delay])
}
})
)
API Types
interface Schedule<Output, Input, Error, Env> {
// Variance markers
readonly [TypeId]: {
readonly _Out: Covariant<Output>
readonly _In: Contravariant<Input>
readonly _Error: Covariant<Error>
readonly _Env: Covariant<Env>
}
}
interface InputMetadata<Input> {
readonly input: Input
readonly attempt: number
readonly start: number
readonly now: number
readonly elapsed: number
readonly elapsedSincePrevious: number
}
interface Metadata<Output, Input> extends InputMetadata<Input> {
readonly output: Output
readonly duration: Duration.Duration
}