| name | effect-patterns-scheduling-periodic-tasks |
| description | Effect-TS patterns for Scheduling Periodic Tasks. Use when working with scheduling periodic tasks in Effect-TS applications. |
Effect-TS Patterns: Scheduling Periodic Tasks
This skill provides 3 curated Effect-TS patterns for scheduling periodic tasks.
Use this skill when working on tasks related to:
- scheduling periodic tasks
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟡 Intermediate Patterns
Scheduling Pattern 4: Debounce and Throttle Execution
Rule: Use debounce to wait for silence before executing, and throttle to limit execution frequency, both critical for handling rapid events.
Good Example:
This example demonstrates debouncing and throttling for common scenarios.
import { Effect, Schedule, Ref } from "effect";
interface SearchQuery {
readonly query: string;
readonly timestamp: Date;
}
const performSearch = (query: string): Effect.Effect<string[]> =>
Effect.gen(function* () {
yield* Effect.log(`[API] Searching for: "${query}"`);
yield* Effect.sleep("100 millis");
return [
`Result 1 for ${query}`,
`Result 2 for ${query}`,
`Result 3 for ${query}`,
];
});
const program = Effect.gen(function* () {
console.log(`\n[DEBOUNCE/THROTTLE] Handling rapid events\n`);
console.log(`[1] Debounced search (wait for silence):\n`);
const searchQueries = ["h", "he", "hel", "hell", "hello"];
const debouncedSearches = yield* Ref.make<Effect.Effect<string[]>[]>([]);
for (const query of searchQueries) {
yield* Effect.log(`[INPUT] User typed: "${query}"`);
yield* Effect.sleep("150 millis");
}
yield* Effect.log(`[DEBOUNCE] User silent for 200ms, executing search`);
const searchResults = yield* performSearch("hello");
yield* Effect.log(`[RESULTS] ${searchResults.length} results found\n`);
console.log(`[2] Throttled scroll handler (max 10/sec):\n`);
const scrollEventCount = yield* Ref.make(0);
const updateCount = yield* Ref.make(0);
for (let i = 0; i < 100; i++) {
yield* Ref.update(scrollEventCount, (c) => c + 1);
if (i % 10 === 0) {
yield* Ref.update(updateCount, (c) => c + 1);
}
}
const events = yield* Ref.get(scrollEventCount);
const updates = yield* Ref.get(updateCount);
yield* Effect.log(
`[THROTTLE] ${events} scroll events → ${updates} updates (${(updates / events * 100).toFixed(1)}% update rate)\n`
);
console.log(`[3] Deduplicating rapid events:\n`);
const userClicks = ["click", "click", "click", "dblclick", "click"];
const lastClick = yield* Ref.make<string | null>(null);
const clickCount = yield* Ref.make(0);
for (const click of userClicks) {
const prev = yield* Ref.get(lastClick);
if (click !== prev) {
yield* Effect.log(`[CLICK] Processing: ${click}`);
yield* Ref.update(clickCount, (c) => c + 1);
yield* Ref.set(lastClick, click);
} else {
yield* Effect.log(`[CLICK] Duplicate: ${click} (skipped)`);
}
}
const processed = yield* Ref.get(clickCount);
yield* Effect.log(
`\n[DEDUPE] ${userClicks.length} clicks → ${processed} processed\n`
);
console.log(`[4] Throttled retry on errors:\n`);
let retryCount = 0;
const operation = Effect.gen(function* () {
retryCount++;
if (retryCount < 3) {
yield* Effect.fail(new Error("Still failing"));
}
yield* Effect.log(`[SUCCESS] Succeeded on attempt ${retryCount}`);
return "done";
}).pipe(
Effect.retry(
Schedule.exponential("100 millis").pipe(
Schedule.upTo("1 second"),
Schedule.recurs(5)
)
)
);
yield* operation;
});
Effect.runPromise(program);
Rationale:
Debounce and throttle manage rapid events:
- Debounce: Wait for silence (delay after last event), then execute once
- Throttle: Execute at most once per interval
- Deduplication: Skip duplicate events
- Rate limiting: Limit events per second
Pattern: Schedule.debounce(duration) or Schedule.throttle(maxEvents, duration)
Rapid events without debounce/throttle cause problems:
Debounce example: Search input
- User types "hello" character by character
- Without debounce: 5 API calls (one per character)
- With debounce: 1 API call after user stops typing
Throttle example: Scroll events
- Scroll fires 100+ times per second
- Without throttle: Updates lag, GC pressure
- With throttle: Update max 60 times per second
Real-world issues:
- API overload: Search queries hammer backend
- Rendering lag: Too many DOM updates
- Resource exhaustion: Event handlers never catch up
Debounce/throttle enable:
- Efficiency: Fewer operations
- Responsiveness: UI stays smooth
- Resource safety: Prevent exhaustion
- Sanity: Predictable execution
Scheduling Pattern 3: Schedule Tasks with Cron Expressions
Rule: Use cron expressions to schedule periodic tasks at specific calendar times, enabling flexible scheduling beyond simple fixed intervals.
Good Example:
This example demonstrates scheduling a daily report generation using cron, with timezone support.
import { Effect, Schedule, Console } from "effect";
import { DateTime } from "luxon";
interface ReportConfig {
readonly cronExpression: string;
readonly timezone?: string;
readonly jobName: string;
}
interface ScheduledReport {
readonly timestamp: Date;
readonly jobName: string;
readonly result: string;
}
const parseCronExpression = (
expression: string
): {
minute: number[];
hour: number[];
dayOfMonth: number[];
month: number[];
dayOfWeek: number[];
} => {
const parts = expression.split(" ");
const parseField = (field: , : ): [] => {
(field === ) {
.({ : max + }, i);
}
(field.()) {
field.().( (part, max));
}
(field.()) {
[start, end] = field.().();
.({ : end - start + }, start + i);
}
[(field)];
};
{
: (parts[], ),
: (parts[], ),
: (parts[], ),
: (parts[], ),
: (parts[], ),
};
};
shouldRunNow = (: < parseCronExpression>): {
now = ();
(
parsed..(now.()) &&
parsed..(now.()) &&
parsed..(now.()) &&
parsed..(now.() + ) &&
parsed..(now.())
);
};
generateReport = (: ): .<> =>
.(* () {
* .();
* .();
{
: (),
jobName,
: ,
};
});
= () =>
.(* () {
parsed = (config.);
* .(
);
* .();
* .();
schedule = .().(
.(
.(* () {
isPastTime = (parsed);
(isPastTime) {
* .(
);
;
}
;
})
)
);
* (config.).(
.(schedule)
);
});
program = .(* () {
.(
);
jobs = [
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
];
* .();
jobs.( {
.(
);
});
});
.(program);
Rationale:
Use cron expressions for scheduling that aligns with business calendars:
- Hourly backups:
0 * * * * (at :00 every hour)
- Daily reports:
0 9 * * 1-5 (9 AM weekdays)
- Monthly cleanup:
0 0 1 * * (midnight on 1st of month)
- Business hours:
0 9-17 * * 1-5 (9 AM-5 PM, Mon-Fri)
Format: minute hour day month weekday
Fixed intervals don't align with business needs:
Fixed interval (every 24 hours):
- If task takes 2 hours, next run is 26 hours later
- Drifts over time
- No alignment with calendar
- Fails during daylight saving time changes
Cron expressions:
- Specific calendar times (e.g., always 9 AM)
- Independent of execution duration
- Aligns with business hours
- Natural DST handling (clock adjusts, cron resyncs)
- Human-readable vs. milliseconds
Real-world example: Daily report at 9 AM
- Fixed interval: Scheduled at 9:00, takes 1 hour → next at 10:00 → drift until 5 PM
- Cron
0 9 * * *: Always runs at 9:00 regardless of duration or previous delays
🟠 Advanced Patterns
Scheduling Pattern 5: Advanced Retry Chains and Circuit Breakers
Rule: Use retry chains with circuit breakers to handle complex failure scenarios, detect cascade failures early, and prevent resource exhaustion.
Good Example:
This example demonstrates circuit breaker and fallback chain patterns.
import { Effect, Schedule, Ref, Data } from "effect";
class RetryableError extends Data.TaggedError("RetryableError")<{
message: string;
code: string;
}> {}
class NonRetryableError extends Data.TaggedError("NonRetryableError")<{
message: string;
code: string;
}> {}
class CircuitBreakerOpenError extends Data.TaggedError("CircuitBreakerOpenError")<{
message: string;
}> {}
interface CircuitBreakerState {
status: "closed" | "open" | "half-open";
failureCount: number;
lastFailureTime: Date | null;
successCount: number;
}
const createCircuitBreaker = (: {
failureThreshold: ;
resetTimeoutMs: ;
halfOpenRequests: ;
}) =>
.(* () {
state = * .<>({
: ,
: ,
: ,
: ,
});
recordSuccess = .(* () {
* .(state, {
(s. === ) {
[
,
{
...s,
: s. + ,
: s. + >= config.
?
: ,
: ,
},
];
}
[, s];
});
});
recordFailure = .(* () {
* .(state, {
newFailureCount = s. + ;
newStatus = newFailureCount >= config.
?
: s.;
[
,
{
...s,
: newFailureCount,
: (),
: newStatus,
},
];
});
});
canExecute = .(* () {
current = * .(state);
(current. === ) {
;
}
(current. === ) {
timeSinceFailure = .() - (current.?.() ?? );
(timeSinceFailure > config.) {
* .(state, [
,
{
...s,
: ,
: ,
: ,
},
]);
;
}
;
}
;
});
{ recordSuccess, recordFailure, canExecute, state };
});
program = .(* () {
.();
cb = * ({
: ,
: ,
: ,
});
.();
requestCount = ;
= () =>
.(* () {
canExecute = * cb.;
(!canExecute) {
* .(
({
: ,
})
);
}
requestCount++;
(shouldFail) {
* cb.;
* .(
);
* .(
({
: ,
: ,
})
);
} {
* cb.;
* .(
);
;
}
});
failSequence = [, , , , , ];
( shouldFail failSequence) {
* (shouldFail).(
.(
.(* () {
(error. === ) {
* .(
);
} {
* .(
);
}
})
)
);
* .();
}
.();
endpoints = {
: ,
: ,
: ,
};
= () =>
.(* () {
* .();
(shouldFail) {
* .();
* .(
({
: ,
: ,
})
);
}
* .();
;
});
fallbackChain = (, ).(
.( (, )),
.( {
* .();
.(endpoints.);
})
);
result = * fallbackChain;
* .();
.();
= () => {
([, , ].(code)) {
;
}
([, ].(code)) {
;
}
([, , ].(code)) {
;
}
(code === ) {
;
}
;
};
errorCodes = [, , , , ];
( code errorCodes) {
classification = (code);
shouldRetry = !classification.();
* .(
);
}
.();
bulkheads = {
: { : , : },
: { : , : },
};
= () =>
.(* () {
bulkhead = bulkheads[endpoint keyof bulkheads];
(!bulkhead) {
;
}
(bulkhead. < bulkhead.) {
bulkhead.++;
;
}
* .(
);
;
});
( i = ; i < ; i++) {
endpoint = i < ? : ;
acquired = * (endpoint);
(acquired) {
* .(
);
}
}
});
.(program);
Rationale:
Advanced retry strategies handle multiple failure types:
- Circuit breaker: Stop retrying when error rate is high
- Bulkhead: Limit concurrency per operation
- Fallback chain: Try multiple approaches in order
- Adaptive retry: Adjust strategy based on failure pattern
- Health checks: Verify recovery before resuming
Pattern: Combine Schedule.retry, Ref state, and error classification
Simple retry fails in production:
Scenario 1: Cascade Failure
- Service A calls Service B (down)
- Retries pile up, consuming resources
- A gets overloaded trying to recover B
- System collapses
Scenario 2: Mixed Failures
- 404 (not found) - retrying won't help
- 500 (server error) - retrying might help
- Network timeout - retrying might help
- Same retry strategy for all = inefficient
Scenario 3: Thundering Herd
- 10,000 clients all retrying at once
- Server recovers, gets hammered again
- Needs coordinated backoff + jitter
Solutions:
Circuit breaker:
- Monitor error rate
- Stop requests when high
- Resume gradually
- Prevent cascade failures
Fallback chain:
- Try primary endpoint
- Try secondary endpoint
- Use cache
- Return degraded result
Adaptive retry:
- Classify error type
- Use appropriate strategy
- Skip unretryable errors
- Adjust backoff dynamically