Documentation Index
Fetch the complete documentation index at: https://effect-ts-effect-smol.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
The Schedule module provides utilities for creating and composing schedules that control retry logic, repetition patterns, and various timing strategies.
Overview
A Schedule<Output, Input, Error, Env> is a function that:
- Takes an input and returns a decision whether to continue or halt
- Provides a delay duration before the next attempt
- Can be combined, transformed, and composed with other schedules
- Works seamlessly with Effect.retry and Effect.repeat
Creating Schedules
Basic Schedules
import { Schedule } from "effect"
// Retry forever with no delay
const forever = Schedule.forever
// Retry a specific number of times
const recurs = Schedule.recurs(3)
// Fixed delay between retries
const spaced = Schedule.spaced("1 second")
// Exponential backoff
const exponential = Schedule.exponential("100 millis", 2.0)
Time-Based Schedules
import { Schedule } from "effect"
// Linear delay (100ms, 200ms, 300ms, ...)
const linear = Schedule.linear("100 millis")
// Fibonacci delay
const fibonacci = Schedule.fibonacci("10 millis")
// Exponential with base delay
const exponential = Schedule.exponential("100 millis")
Using Schedules
With Effect.retry
import { Effect, Schedule } from "effect"
const retryPolicy = Schedule.exponential("100 millis").pipe(
Schedule.compose(Schedule.recurs(3))
)
const program = Effect.gen(function*() {
const result = yield* Effect.retry(
Effect.fail("Network error"),
retryPolicy
)
return result
})
With Effect.repeat
import { Effect, Schedule } from "effect"
const heartbeat = Effect.log("heartbeat").pipe(
Effect.repeat(Schedule.spaced("30 seconds"))
)
Composing Schedules
Intersection (Both)
Continues while both schedules want to continue:
import { Schedule } from "effect"
// Retry up to 5 times with 1 second spacing
const schedule = Schedule.intersect(
Schedule.recurs(5),
Schedule.spaced("1 second")
)
// Or using pipe
const schedule2 = Schedule.recurs(5).pipe(
Schedule.intersect(Schedule.spaced("1 second"))
)
Union (Either)
Continues while either schedule wants to continue:
import { Schedule } from "effect"
// Continue for 1 minute OR 10 attempts, whichever is longer
const schedule = Schedule.union(
Schedule.elapsed.pipe(Schedule.whileOutput((d) => d < "1 minute")),
Schedule.recurs(10)
)
Compose (Sequential)
Runs first schedule, then second schedule:
import { Schedule } from "effect"
// Fast retries first, then slow retries
const schedule = Schedule.compose(
Schedule.recurs(3).pipe(Schedule.addDelay(() => "100 millis")),
Schedule.recurs(5).pipe(Schedule.addDelay(() => "1 second"))
)
Map Output
import { Effect, Schedule } from "effect"
const schedule = Schedule.recurs(5).pipe(
Schedule.map((n) => Effect.succeed(`Attempt ${n}`))
)
Add Delay
import { Effect, Schedule } from "effect"
const schedule = Schedule.recurs(3).pipe(
Schedule.addDelay((n) => Effect.succeed(`${n * 100} millis`))
)
While/Until Conditions
import { Schedule } from "effect"
// Continue while condition is true
const whileSchedule = Schedule.recurs(10).pipe(
Schedule.whileOutput((n) => n < 5)
)
// Continue until condition is true
const untilSchedule = Schedule.recurs(10).pipe(
Schedule.untilOutput((n) => n >= 5)
)
Common Patterns
Exponential Backoff with Cap
import { Schedule } from "effect"
const retryPolicy = Schedule.exponential("100 millis", 2.0).pipe(
Schedule.intersect(
Schedule.spaced("10 seconds") // Maximum delay
),
Schedule.compose(Schedule.recurs(5)) // Maximum attempts
)
Retry with Jitter
import { Effect, Schedule } from "effect"
const retryPolicy = Schedule.exponential("100 millis").pipe(
Schedule.jittered, // Adds randomness to prevent thundering herd
Schedule.compose(Schedule.recurs(3))
)
Conditional Retry
import { Effect, Schedule } from "effect"
const retryPolicy = Schedule.exponential("100 millis").pipe(
Schedule.whileInput<Error>((error) =>
error.message.includes("temporary")
),
Schedule.compose(Schedule.recurs(5))
)
const program = Effect.gen(function*() {
const result = yield* Effect.retry(
riskyOperation,
retryPolicy
)
return result
})
Polling Pattern
import { Effect, Schedule } from "effect"
const pollSchedule = Schedule.spaced("5 seconds").pipe(
Schedule.compose(Schedule.recurs(10))
)
const checkStatus = Effect.gen(function*() {
const status = yield* fetchStatus()
if (status === "complete") {
return status
}
return yield* Effect.fail("not ready")
})
const program = Effect.retry(checkStatus, pollSchedule)
import { Console, Effect, Schedule } from "effect"
const schedule = Schedule.spaced("1 second").pipe(
Schedule.collectWhile((metadata) =>
Effect.gen(function*() {
yield* Console.log(`Attempt: ${metadata.attempt}`)
yield* Console.log(`Elapsed: ${metadata.elapsed}ms`)
return metadata.attempt <= 5
})
)
)
Advanced Schedules
Custom Schedule
import { Effect, Schedule } from "effect"
const customSchedule = Schedule.unfold(0, (n) =>
Effect.succeed(n + 1)
).pipe(
Schedule.addDelay((n) => Effect.succeed(`${n * 100} millis`)),
Schedule.whileOutput((n) => n < 10)
)
Time-of-Day Schedule
import { Effect, Schedule } from "effect"
const businessHours = Schedule.spaced("1 hour").pipe(
Schedule.collectWhile((metadata) =>
Effect.sync(() => {
const hour = new Date().getHours()
return hour >= 9 && hour < 17 // 9 AM to 5 PM
})
)
)
Circuit Breaker Pattern
import { Effect, Schedule } from "effect"
const circuitBreaker = Schedule.exponential("1 second").pipe(
Schedule.whileOutput((delay) => delay < "1 minute"),
Schedule.onComplete(() =>
Effect.log("Circuit breaker opened")
)
)
Monitoring Schedules
Tap on Each Iteration
import { Effect, Schedule } from "effect"
const schedule = Schedule.recurs(5).pipe(
Schedule.tapOutput((n) =>
Effect.log(`Retry attempt: ${n}`)
)
)
Log on Completion
import { Effect, Schedule } from "effect"
const schedule = Schedule.recurs(3).pipe(
Schedule.onComplete(() =>
Effect.log("Schedule completed")
)
)
Best Practices
- Combine schedules: Use intersect/union to create sophisticated retry logic
- Add jitter: Use jittered to prevent thundering herd problems
- Cap maximum delays: Prevent infinite backoff with maximum delay limits
- Limit retry attempts: Always set a maximum number of retries
- Use conditional retries: Only retry on recoverable errors
- Monitor schedule execution: Log attempts and delays for debugging
Common Use Cases
API Rate Limiting
import { Effect, Schedule } from "effect"
const rateLimited = Schedule.spaced("100 millis").pipe(
Schedule.compose(Schedule.recurs(10))
)
const apiCall = Effect.retry(
callAPI(),
rateLimited
)
Database Connection Retry
import { Effect, Schedule } from "effect"
const dbRetry = Schedule.exponential("1 second").pipe(
Schedule.intersect(Schedule.spaced("30 seconds")), // Max 30s delay
Schedule.compose(Schedule.recurs(5))
)
const connect = Effect.retry(
connectToDatabase(),
dbRetry
)
Health Check Polling
import { Effect, Schedule } from "effect"
const healthCheck = Effect.repeat(
checkServiceHealth(),
Schedule.spaced("30 seconds")
)
Next Steps
- Learn about Effect for error handling
- Explore Stream for processing sequences
- Understand retry strategies in production applications