task-scheduling
Skill118 starsChanged 16 days agoMITView on GitHub
This skill should be used when the user designs a "task scheduler", "job scheduler", "job queue", "cron at scale", "distributed cron", "delayed / scheduled / recurring tasks", a "worker pool", reaches for "Celery / Sidekiq / Airflow", or wrestles with "task leasing", visibility timeouts, job priorities, fairness, or duplicate task execution. Use it whenever work must run later, on a schedule, or be reliably leased to a pool of workers, even if the user doesn't say "scheduler". (For plain queue transport and delivery guarantees, that is `messaging-streaming`.)
In this repository21 other files it gives its agents.
- api-designSkill
- architecture-diagramSkill
- back-of-the-envelopeSkill
- blob-storeSkill
- cachingSkill
- consistency-coordinationSkill
- content-deliverySkill
- data-storageSkill
- distributed-loggingSkill
- distributed-searchSkill
- dnsSkill
- load-balancingSkill
- messaging-streamingSkill
- observabilitySkill
- requirements-scopingSkill
- resilience-failureSkill
- scaling-evolutionSkill
- sequencerSkill
- service-decompositionSkill
- sharded-countersSkill
- system-designSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 7 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 143
Pick a provider: Default to the generic recipe.
- Distinctive: line 152
Default to pull-based worker leasing with a visibility timeout.
- Distinctive: line 159
Don't assume a leased job ran once — at-least-once means design for duplicates.
- Distinctive: line 160
Don't schedule every cron job at :00 — jitter so the tick doesn't stampede.
- Distinctive: line 161
Don't run two schedulers without leader election (split-brain double-fires).
- Distinctive: line 162
Don't retry a poison task forever.
- Distinctive: line 163
Don't reach for Airflow/Celery before a schedule, priority, or HA need is real (YAGNI).
- Very specific: line 187
Default to the generic recipe above.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Task Scheduling
Decide when work runs and which worker runs it: fire jobs on a schedule
(cron/delayed/recurring), hand each job to exactly one worker via a lease, and
make sure it completes once despite crashes and retries. This sits on top of
messaging-streaming queues — the queue is the transport; this skill adds the
scheduling, leasing, priorities, and task-level idempotency. Getting it wrong
shows up as jobs that never run, run twice (double charge, double email), or
pile up until a worker fleet falls permanently behind.
When to reach for this
Work must run later (send a reminder in 24h), on a schedule (nightly
rollups, hourly cron), or repeatedly (poll every 5 min); a slow operation is
already off the request path (→ messaging-streaming) and now needs reliable
allocation to a pool of workers; jobs need priorities (paid before free) or
fairness (no single tenant starves others); or a job must complete exactly
once even though the worker holding it can crash mid-flight.
When NOT to
The caller needs the result inline — that's a synchronous call, not a scheduled
job. A single fire-and-forget async step with no schedule, priority, or
exactly-once need — a plain queue + idempotent consumer (messaging-streaming)
is simpler; don't add a scheduler on top. One periodic job on one box —
OS cron is fine until you have multiple schedulers or need history and
retries. A long-running multi-step saga with rollback — reach for a durable
workflow engine instead of hand-rolling state across jobs. Don't stand up
Airflow/Celery "because we'll have batch jobs eventually" (YAGNI): it's a
stateful control plane to operate and monitor.
Clarify first
- Trigger type — scheduled (cron/at a time), delayed (run after N seconds), recurring (every N), or event-driven (a queue message arrives)? This decides whether a scheduler is even in scope.
- Exactly-once vs at-least-once — is a duplicate run harmful (money, email) or harmless (idempotent recompute)? Drives the leasing + dedup design.
- Latency budget vs throughput — must a delayed job fire within seconds of its time, or is "within a few minutes" fine? Tight timing is more expensive.
- Priority / fairness — do some jobs jump the line, and must one tenant or
job class be prevented from starving the rest? (→
back-of-the-envelopefor arrival vs. service rate.) - Job duration & variance — seconds or hours? Sets the visibility-timeout / lease length and whether long jobs need heartbeats.
- Idempotency key — what identifies a task as the same task on retry?
(The key contract is owned by
api-design.)
The options
Scheduling trigger
- OS cron / single scheduler — one process fires jobs on a crontab. Use when one node, a handful of jobs, no HA requirement.
- Distributed scheduler (HA cron) — a leader-elected scheduler enqueues due jobs into a queue; followers stand by. Use when the schedule must survive a node loss and must not double-fire.
- Delay queue / timer — jobs carry a "not before" time; the queue holds them until due (delivery delay, sorted-set scoring, or a timer wheel). Use when per-job delays vary and you don't want a cron tick.
- Workflow/orchestration DAG — declared task dependencies with backfill and history (Airflow-style). Use when batch pipelines have dependencies and you need a run history and reruns.
Worker allocation
- Pull (worker leasing) — workers poll the queue, lease a job for a visibility timeout, and ack/delete on success. Use when you want back-pressure for free and elastic, self-balancing workers. The default.
- Push (dispatcher assigns) — a coordinator routes jobs to specific workers. Use when affinity/locality matters (a job must run where its data is).
Priority & fairness
- Priority queues — separate high/low queues drained in order. Use when some classes must run first.
- Weighted / fair scheduling — round-robin or weighted draw across per-tenant queues. Use when one tenant's burst must not starve others.
Trade-offs
| Option | What it solves | What it worsens | Change it when |
| OS cron / single scheduler | Trivial; zero infra | SPOF — node dies, schedule stops; no retry/history | You need HA or missed-run recovery → distributed scheduler |
| Distributed scheduler (HA cron) | Survives node loss; no double-fire (leader-elected) | Needs leader election (→ consistency-coordination); more moving parts | One box and one job is enough → OS cron |
| Delay queue / timer | Per-job delays without a cron tick; precise-ish timing | Far-future jobs sit in the queue; timer accuracy bounded by poll interval | Delays are uniform/periodic → cron; dependencies exist → DAG |
| Workflow DAG (Airflow-style) | Dependencies, backfill, run history, reruns | Heavy control plane; scheduler latency; overkill for single jobs | Jobs are independent one-shots → plain queue + scheduler |
| Pull (worker leasing) | Self-balancing, elastic, natural back-pressure | At-least-once: lease expiry on a slow job re-runs it (need idempotency) | A job must run on a specific node (data locality) → push |
| Push (dispatcher) | Affinity/locality; central control | Dispatcher is a bottleneck/SPOF; must track worker health | No locality need → pull is simpler |
| Priority queues | Important work runs first | Low-priority starvation under sustained load | Fairness across tenants matters → weighted/fair |
| Weighted / fair scheduling | No tenant starves another | More complex; per-tenant accounting | Only one workload class exists → single queue |
Behavior under stress
A scheduler can quietly fall behind, or it can amplify an outage by re-dispatching work a struggling fleet can't finish.
- Backlog growth: arrival rate exceeds worker throughput; due jobs queue up and "scheduled for 09:00" runs at 09:40. End-to-end delay climbs while CPU looks fine. Mitigate: alarm on oldest-due-job age and queue depth, not just rate; scale workers; shed or defer low-priority jobs.
- Lease expiry / re-run storm (the classic): a job runs longer than its visibility timeout, the lease expires, the queue redelivers it to a second worker, and now two workers run it — wasting capacity and, without idempotency, double-applying side effects. Mitigate: set the timeout above p99 job duration, heartbeat to extend the lease on long jobs, and make tasks idempotent/deduped.
- Poison task: a job that always fails is retried forever, burning workers
and re-loading a sick downstream. Mitigate: cap retries with backoff+jitter,
then route to a dead-letter queue (retries/backoff/DLQ are owned by
resilience-failure). - Thundering herd at the tick: thousands of cron jobs all scheduled at the top of the hour fire at once and stampede a downstream. Mitigate: jitter the schedule, spread triggers, or rate-limit dispatch.
- Scheduler split-brain: two schedulers both think they're leader and
double-enqueue every recurring job. Mitigate: a single leader via leader
election + fencing (→
consistency-coordination); idempotent enqueue keyed by (job, scheduled_time). - Starvation: a flood of high-priority or one noisy tenant's jobs starves everyone else. Mitigate: fair/weighted scheduling and per-tenant concurrency caps.
Monitor: oldest-due-job age (the best lateness signal), queue depth per priority, lease-expiry / redelivery rate, retry and DLQ rate, worker utilization, and per-tenant share.
How to apply
- Clarify the inputs. Settle trigger type, exactly-once vs at-least-once,
latency budget, priority/fairness, and job duration (see Clarify first). If
the work is a single async step with no schedule or priority, stop — a plain
queue + idempotent consumer (
messaging-streaming) is enough. - Pick from the trade-off table. Choose a trigger (cron → distributed scheduler → delay queue → DAG, cheapest that fits), an allocation model (pull leasing is the default; push only for locality), and a priority/fairness model only if more than one class exists.
- Set the key knobs. Visibility timeout above p99 job duration, retry cap + backoff before the DLQ, the dedup/idempotency key per task, the lease heartbeat interval for long jobs, and per-tenant concurrency limits.
- Stress-test the choice. Walk backlog growth, lease-expiry re-runs, poison tasks, tick stampede, scheduler split-brain, and starvation. Confirm a mitigation exists for each one the workload can trigger.
- Size it with numbers. Workers needed = arrival rate × avg job seconds / target concurrency (Little's law); confirm sustained throughput drains peak arrival, and that far-future delayed jobs fit storage (→ Numbers that matter).
- Pick a provider. Default to the generic recipe; only open a provider file if the user named a cloud (see Choosing a provider).
- Show the trade-off in the answer — give a
Decision | Solves | Worsens | Change it when…table with one row per choice you made here (e.g. the option, its store, its failure policy); the last column is a concrete trigger (a number or observable signal). Then name what breaks first.
Dos and don'ts
Do
- Default to pull-based worker leasing with a visibility timeout; it self-balances.
- Set the visibility timeout above p99 job duration and heartbeat-extend long jobs.
- Make every task idempotent (dedup on a task key) so a re-run is harmless.
- Elect a single leader for the scheduler and key recurring enqueues by (job, time).
- Cap retries with backoff+jitter, then dead-letter; alarm on oldest-due-job age.
Don't
- Don't assume a leased job ran once — at-least-once means design for duplicates.
- Don't schedule every cron job at :00 — jitter so the tick doesn't stampede.
- Don't run two schedulers without leader election (split-brain double-fires).
- Don't retry a poison task forever; cap it and dead-letter it.
- Don't reach for Airflow/Celery before a schedule, priority, or HA need is real (YAGNI).
Numbers that matter
Size the worker pool with Little's law: in-flight jobs = arrival rate × average
job duration, so workers ≈ peak arrival × avg seconds-per-job / per-worker
concurrency. Sustained drain must exceed peak arrival or the backlog never
clears. Set the visibility timeout above the p99 job duration (a too-short
timeout is the #1 cause of duplicate runs); set far-future delay storage =
delayed-job rate × max delay × job size. Don't restate the latency/QPS tables —
pull the rates and durations from back-of-the-envelope.
Interface sketch
A scheduled task is a contract. Define it, not "a job":
- Task envelope: stable
task_id/ idempotency key (dedup on retry — key contract owned byapi-design),task_type/version,payload,priority,scheduled_for(run-not-before),attempt, and atrace_id. - Schedule spec: for recurring jobs, the cron/interval expression plus a
dedup_key = (job_name, scheduled_time)so a double-enqueue is a no-op. - Lease contract: on dequeue a worker gets the task invisible for
visibility_timeout; it must ack/delete on success or heartbeat to extend. Lease expiry → automatic redelivery. Failure after the retry cap → DLQ with attempt count and last error.
Choosing a provider
Default to the generic recipe above. If the user names a cloud, read
references/providers/<provider>.md for the managed-service mapping,
quotas/limits, and provider-specific trade-offs. If no file exists for that
provider, the generic recipe is the answer.
Diagram
To visualize the scheduler → queue → leased workers path, or the lease-expiry
redelivery loop, use the in-plugin architecture-diagram skill. Quick inline
sketch: [scheduler] → [delay/priority queue] → workers (lease) ─expire→ requeue ─fail×N→ [DLQ];
main path solid, the expiry/DLQ branches dashed.
Related building blocks
messaging-streaming— depends on it: queues are the transport this skill schedules onto and leases from; it owns delivery guarantees, ordering, and DLQs and this skill does not reimplement them.resilience-failure— pairs with it for the retry policy: backoff, jitter, and DLQ-as-containment are tuned there; this skill names them.api-design— depends on its idempotency-key contract, the mechanism that makes a re-run after lease expiry safe.consistency-coordination— depends on it for leader election (and fencing) so only one scheduler is active and recurring jobs don't double-fire.back-of-the-envelope— feeds into sizing: arrival rate and job duration set the worker count and backlog drain.system-design— feeds into the orchestrator's reasoning loop; it routes here when work must run later, on a schedule, or be reliably allocated to workers.
References
references/deep-dive.md— leasing/visibility-timeout mechanics and heartbeats, distributed-cron + leader election, delay-queue implementations (sorted set, timer wheel), priority/fairness algorithms, exactly-once-effect via dedup, and Celery/Sidekiq/Airflow internals. Read when designing the scheduler in detail.references/providers/{generic,aws,azure,gcp,temporal}.md— service mappings, decision-changing limits, and pitfalls per environment.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

