| name | effect-patterns-scheduling |
| description | Effect-TS patterns for Scheduling. Use when working with scheduling in Effect-TS applications. |
Effect-TS Patterns: Scheduling
This skill provides 3 curated Effect-TS patterns for scheduling.
Use this skill when working on tasks related to:
- scheduling
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟢 Beginner Patterns
Retry Failed Operations
Rule: Use Effect.retry with a Schedule to handle transient failures gracefully.
Good Example:
import { Effect, Schedule, Data } from "effect"
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly message: string
}> {}
class RateLimitError extends Data.TaggedError("RateLimitError")<{
readonly retryAfter: number
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly resource: string
}> {}
let callCount = 0
const fetchData = Effect.gen(function* () {
callCount++
yield* Effect.log(`API call attempt ${callCount}`)
if (callCount < 3) {
return yield* Effect.fail(new NetworkError({ message: "Connection timeout" }))
}
return { data: "Success!", attempts: callCount }
})
const withBasicRetry = fetchData.pipe(
Effect.retry(Schedule.recurs(5))
)
const withDelayedRetry = fetchData.pipe(
Effect.retry(
Schedule.spaced("500 millis").pipe(
Schedule.intersect(Schedule.recurs(5))
)
)
)
const fetchWithErrors = (shouldFail: boolean) =>
Effect.gen(function* () {
if (shouldFail) {
const random = Math.random()
if (random < 0.5) {
return yield* Effect.fail(new NetworkError({ message: "Timeout" }))
} else if (random < 0.8) {
return yield* Effect.fail(new RateLimitError({ retryAfter: 1000 }))
} else {
return yield* Effect.fail(new NotFoundError({ resource: "user:123" }))
}
}
return "Data fetched!"
})
const retryTransientOnly = fetchWithErrors(true).pipe(
Effect.retry({
schedule: Schedule.recurs(3),
while: (error) =>
error._tag === "NetworkError" || error._tag === "RateLimitError",
})
)
const withExponentialBackoff = fetchData.pipe(
Effect.retry(
Schedule.exponential("100 millis", 2).pipe(
Schedule.intersect(Schedule.recurs(5))
)
)
)
const program = Effect.gen(function* () {
yield* Effect.log("Starting retry demo...")
callCount = 0
const result = yield* withBasicRetry
yield* Effect.log(`Final result: ${JSON.stringify(result)}`)
})
Effect.runPromise(program)
Rationale:
Use Effect.retry to automatically retry operations that fail due to transient errors like network timeouts.
Many failures are temporary:
- Network issues - Connection drops, timeouts
- Rate limits - Too many requests
- Resource contention - Database locks
- Service restarts - Brief unavailability
Automatic retries handle these without manual intervention.
Your First Schedule
Rule: Use Schedule to control when and how often effects run.
Good Example:
import { Effect, Schedule } from "effect"
let attempts = 0
const flakyOperation = Effect.gen(function* () {
attempts++
if (attempts < 3) {
yield* Effect.log(`Attempt ${attempts} failed`)
return yield* Effect.fail(new Error("Temporary failure"))
}
return `Success on attempt ${attempts}`
})
const withRetry = flakyOperation.pipe(
Effect.retry(Schedule.recurs(5))
)
const logTime = Effect.gen(function* () {
const now = new Date().toISOString()
yield* Effect.log(`Current time: ${now}`)
return now
})
const repeated = logTime.pipe(
Effect.repeat(Schedule.recurs(3))
)
const polling = logTime.pipe(
Effect.repeat(
Schedule.spaced("1 second").pipe(
Schedule.intersect(Schedule.recurs(5))
)
)
)
const fixedDelay = Schedule.spaced("500 millis")
const exponentialBackoff = Schedule.exponential("1 second")
const limitedAttempts = Schedule.recurs(3)
const retryPolicy = Schedule.exponential("100 millis").pipe(
Schedule.intersect(Schedule.recurs(5))
)
const program = Effect.gen(function* () {
yield* Effect.log("--- Retry Example ---")
const result = yield* withRetry
yield* Effect.log(`Result: ${result}`)
yield* Effect.log("\n--- Repeat Example ---")
yield* repeated
})
Effect.runPromise(program)
Rationale:
Use Schedule to control timing in Effect programs - retrying failed operations, repeating successful ones, or adding delays.
Schedules solve common timing problems:
- Retries - Try again after failures
- Polling - Check for updates periodically
- Rate limiting - Control how fast things run
- Backoff - Increase delays between attempts
🟡 Intermediate Patterns
Scheduling Pattern 1: Repeat an Effect on a Fixed Interval
Rule: Repeat effects at fixed intervals using Schedule.fixed for steady-state operations and background tasks.
Good Example:
This example demonstrates a health check service that polls multiple service endpoints every 30 seconds and reports their status.
import { Effect, Schedule, Duration } from "effect";
interface ServiceStatus {
readonly service: string;
readonly url: string;
readonly isHealthy: boolean;
readonly responseTime: number;
readonly lastChecked: number;
}
const checkServiceHealth = (
url: string,
service: string
): Effect.Effect<ServiceStatus> =>
Effect.gen(function* () {
const startTime = Date.now();
const isHealthy = Math.random() > 0.1;
const responseTime = Math.random() * 500;
yield* Effect.sleep(Duration.millis(Math.round(responseTime)));
if (!isHealthy) {
yield* Effect.fail(new Error(`${service} is unhealthy`));
}
return {
service,
url,
isHealthy: true,
responseTime: Math.round(Date.now() - startTime),
lastChecked: Date.now(),
};
});
interface HealthCheckConfig {
readonly services: Array<{
readonly name: string;
readonly url: string;
}>;
readonly intervalSeconds: number;
}
const serviceStatuses = new Map<string, ServiceStatus>();
const checkAllServices = (
config: HealthCheckConfig
): Effect.Effect<void> =>
Effect.gen(function* () {
for (const service of config.services) {
const status = yield* checkServiceHealth(service.url, service.name).pipe(
Effect.either
);
if (status._tag === "Right") {
serviceStatuses.set(service.name, status.right);
console.log(
`✓ ${service.name}: OK (${status.right.responseTime}ms)`
);
} else {
console.log(`✗ ${service.name}: FAILED`);
}
}
});
const createHealthCheckScheduler = (
config: HealthCheckConfig
): Effect.Effect<void> =>
checkAllServices(config).pipe(
Effect.repeat(
Schedule.fixed(Duration.seconds(config.intervalSeconds))
)
);
const reportStatus = (): Effect.Effect<void> =>
Effect.sync(() => {
if (serviceStatuses.size === 0) {
console.log("\n[STATUS] No services checked yet");
return;
}
console.log("\n[STATUS REPORT]");
for (const [service, status] of serviceStatuses) {
const ago = Math.round((Date.now() - status.lastChecked) / 1000);
console.log(
` ${service}: ${status.isHealthy ? "✓" : "✗"} (checked ${ago}s ago)`
);
}
});
const program = Effect.gen(function* () {
const config: HealthCheckConfig = {
services: [
{ name: "API", url: "https://api.example.com/health" },
{ name: "Database", url: "https://db.example.com/health" },
{ name: "Cache", url: "https://cache.example.com/health" },
],
intervalSeconds: 5,
};
const checker = yield* createHealthCheckScheduler(config).pipe(
Effect.fork
);
yield* reportStatus().pipe(
Effect.repeat(
Schedule.addDelay(
Schedule.recurs(3),
() => Duration.seconds(15)
)
)
);
yield* checker.interrupt();
});
Effect.runPromise(program);
This pattern:
- Defines service health checks that may fail
- Uses Schedule.fixed to repeat every 5 seconds
- Handles failures gracefully (keeps last known status)
- Runs in background while main logic continues
- Reports current status at intervals
Rationale:
When you need to run an effect repeatedly at regular intervals (e.g., every 5 seconds, every 30 minutes), use Schedule.fixed to specify the interval. This creates a schedule that repeats the effect indefinitely or until a condition stops it, with precise timing between executions.
Many production systems need periodic operations:
- Health checks: Poll service availability every 30 seconds
- Cache refresh: Update cache every 5 minutes
- Metrics collection: Gather system metrics every 10 seconds
- Data sync: Sync data with remote service periodically
- Cleanup tasks: Remove stale data nightly
Without proper scheduling:
- Manual polling with
while loops wastes CPU (busy-waiting)
- Thread.sleep blocks threads, preventing other work
- No automatic restart on failure
- Difficult to test deterministically
With Schedule.fixed:
- Efficient, non-blocking repetition
- Automatic failure handling and retry
- Testable with TestClock
- Clean, declarative syntax