Retry and Schedule

Retry with Exponential Backoff

import { Effect, Schedule, Console } from "effect"

let attempts = 0

const unstableCall = Effect.gen(function*() {
  attempts++
  yield* Console.log(`Attempt ${attempts}`)
  if (attempts < 3) {
    return yield* Effect.fail("not ready")
  }
  return "success"
})

const program = unstableCall.pipe(
  Effect.retry(Schedule.exponential("1 seconds").pipe(Schedule.jittered))
)

Effect.runPromise(program).then(console.log)
// Output:
// Attempt 1
// Attempt 2
// Attempt 3
// success

Schedule.exponential doubles the delay after each failure. Schedule.jittered adds randomness to prevent thundering herd retries. The combined schedule waits ~1s, ~2s, ~4s (with jitter).

Retry with a Fixed Number of Times

const program = unstableCall.pipe(
  Effect.retry(Schedule.recurs(5))
)

Schedule.recurs(5) retries up to 5 times with no delay between attempts. Combine with a delay schedule for paced retries:

const program = unstableCall.pipe(
  Effect.retry(
    Schedule.spaced("500 millis").pipe(Schedule.compose(Schedule.recurs(5)))
  )
)

HTTP Retry with Backoff and Max Attempts

import { Effect, Schedule, Schema } from "effect"

class HttpError extends Schema.TaggedError<HttpError>()("HttpError", {
  status: Schema.Number
}) {}

const fetchWithRetry = (url: string) =>
  Effect.tryPromise({
    try: () => fetch(url).then((r) => {
      if (r.status >= 500) throw new HttpError({ status: r.status })
      return r.json()
    }),
    catch: (e) => e instanceof HttpError ? e : new HttpError({ status: 0 })
  }).pipe(
    Effect.retry(
      Schedule.exponential("1 seconds")
        .pipe(Schedule.jittered)
        .pipe(Schedule.compose(Schedule.recurs(3)))
    ),
    Effect.catchTag("HttpError", (err) =>
      Effect.succeed({ error: `Failed after retries: HTTP ${err.status}` })
    )
  )

Effect.runPromise(fetchWithRetry("https://api.example.com/data"))

This retries up to 3 times with exponential backoff (1s, 2s, 4s) plus jitter, then catches the final failure if all retries are exhausted.

Cron-Based Recurring Task

import { Effect, Schedule, Console } from "effect"

const heartbeat = Effect.gen(function*() {
  yield* Console.log("Heartbeat tick")
})

const schedule = Schedule.cron("*/5 * * * *")

const program = heartbeat.pipe(
  Effect.repeat(schedule)
)

Effect.runFork(program)

Schedule.cron accepts standard cron expressions. The example runs every 5 minutes. Use Effect.repeat to run an effect on the schedule.

Gotcha: Schedule.cron requires the cron field format: minute, hour, day of month, month, day of week. Not all cron implementations are identical. Check the Effect cron parser for supported syntax.

Compose Schedule Constraints

import { Effect, Schedule, Duration, Console } from "effect"

const task = Console.log("Running task")

// Run every 10 seconds, but stop after 5 minutes total
const schedule = Schedule.spaced("10 seconds").pipe(
  Schedule.compose(Schedule.duration(Duration.minutes(5)))
)

// Run with exponential backoff, max 30s delay, max 10 retries
const retrySchedule = Schedule.exponential("1 seconds").pipe(
  Schedule.compose(Schedule.max("30 seconds")),
  Schedule.compose(Schedule.recurs(10))
)

const program = task.pipe(Effect.repeat(schedule))

const retryProgram = unstableCall.pipe(
  Effect.retry(retrySchedule)
)

Schedule.compose chains schedule constraints. Each constraint filters or transforms the delay pattern:

CombinatorEffect
Schedule.recurs(n)Stop after n iterations
Schedule.duration(d)Stop after total elapsed time
Schedule.max(d)Cap the delay at d
Schedule.jitteredAdd random jitter to delays
Schedule.spaced(d)Fixed delay between iterations
Schedule.exponential(d)Double the delay each iteration

Using Schedule with Effect.repeat

import { Effect, Schedule, Console } from "effect"

let count = 0

const poll = Effect.gen(function*() {
  count++
  yield* Console.log(`Poll ${count}`)
  return count
})

// Repeat 4 times with 1 second spacing
const program = poll.pipe(
  Effect.repeat(Schedule.spaced("1 seconds").pipe(Schedule.compose(Schedule.recurs(4))))
)

Effect.runPromise(program).then(console.log)
// Output (over ~4 seconds):
// Poll 1
// Poll 2
// Poll 3
// Poll 4
// 4

Effect.repeat runs the effect on the schedule and returns the final value. The schedule controls timing and termination.

Next Steps