- Published on
Cron & Scheduled Tasks in 2026 — systemd timers / Vercel Cron / AWS EventBridge Scheduler / k8s CronJobs / BullMQ / Sidekiq / Temporal Deep Dive
- Authors

- Name
- Youngju Kim
- @fjvbn20031
Prologue — When "just run a cron" Became Dangerous
A 2026 team design discussion.
Junior: "We need to send a daily report email at 9 AM. Just a cron job, right?" Senior: "On which box? How do we monitor it? What if it fails? What if it runs twice? What if the host dies?" Junior: "...oh."
This short exchange contains everything about scheduling in 2026. cron worked for 50 years, but in the cloud-native era, "run a command at a fixed time" is no longer enough. Host failure, double-firing concurrency bugs, failure alerts, retries, distributed locking, idempotency, timezones, DST — every one of these became an operational trap.
Today, scheduled tasks split into four big categories. System level (cron, systemd timers, fcron, anacron); cloud managed (Vercel Cron, EventBridge Scheduler, Cloudflare Workers, GitHub Actions schedule); distributed background queues (BullMQ, Sidekiq, Celery Beat); workflow engines (Airflow, Dagster, Prefect, Temporal, Inngest, Trigger.dev, Hatchet). Layered on top: monitoring (Healthchecks.io, Cronitor.io).
This article maps the whole terrain. Classic cron five-field syntax, systemd timers, Vercel Cron, AWS EventBridge Scheduler (which replaced CloudWatch Events Rules in November 2022), k8s CronJobs, BullMQ, Sidekiq, Celery Beat, Temporal Schedules, Hatchet, Quartz, Hangfire, fcron, anacron, Nomad periodic jobs, and monitoring SaaS. Plus how Toss, Kakao, and Mercari run things in production.
1. The 2026 cron Map — System / Cloud / Queue / Workflow
The big picture first. Under the single phrase "scheduled tasks" sit very different tools.
| Category | Representative tools | Best for |
|---|---|---|
| System cron | crond, systemd timers, fcron, anacron | Single-box background chores, log rotation |
| Cloud managed | Vercel Cron, AWS EventBridge Scheduler, Cloudflare Workers Cron Triggers, GitHub Actions schedule, GitLab schedule pipelines | Serverless, CI/CD, infrastructure automation |
| Distributed background queue | BullMQ, Sidekiq, Celery Beat, RQ, dramatiq, APScheduler, Quartz, Spring @Scheduled, Hangfire | In-app job queues plus schedules |
| Workflow engine | Airflow, Dagster, Prefect, Temporal Schedules, Inngest Crons, Trigger.dev v3, Hatchet | Multi-step pipelines, reliability workflows |
| Container | k8s CronJobs, Nomad periodic jobs | Container-based batch jobs |
| Monitoring | Healthchecks.io, Cronitor.io | Dead man's switch, missed-ping alerts |
The 2026 consensus best practices.
- One command on one box → systemd timers (not cron).
- Serverless environment → Vercel Cron, EventBridge Scheduler, or Cloudflare Cron, depending on your infra.
- In-app background plus periodic work → BullMQ / Sidekiq / Celery Beat.
- Multi-step workflow → Temporal / Inngest / Trigger.dev / Hatchet (modern) or Airflow / Dagster / Prefect (traditional).
- Whatever you pick, attach Healthchecks.io / Cronitor.io alerts.
Follow this guide and 99% of the 2026 cases are handled.
2. Classic cron — Five-Field Syntax, /etc/crontab
cron was written by Brian Kernighan in 1975. It survived 50 years and is still installed on almost every Linux box. Start with the core: five fields.
# ┌───────── minute (0-59)
# │ ┌─────── hour (0-23)
# │ │ ┌───── day of month (1-31)
# │ │ │ ┌─── month (1-12)
# │ │ │ │ ┌─ day of week (0-7, both 0 and 7 are Sunday)
# │ │ │ │ │
# * * * * * command-to-execute
Common patterns.
# Daily at midnight
0 0 * * * /usr/local/bin/backup.sh
# Every hour on the hour
0 * * * * /usr/local/bin/sync.sh
# Every 5 minutes
*/5 * * * * /usr/local/bin/healthcheck.sh
# Weekdays 9 AM
0 9 * * 1-5 /usr/local/bin/report.sh
# First of each month, 3 AM
0 3 1 * * /usr/local/bin/monthly-billing.sh
cron has two crontab flavors.
- User crontab: edited with
crontab -e, stored in/var/spool/cron/, runs as the user. - System crontab:
/etc/crontab,/etc/cron.d/*— must include the run-as user as a sixth field.
# /etc/crontab — system. Adds a user field.
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts /etc/cron.daily )
Beyond the five fields, some cron implementations (Vixie cron, anacron, cronie) support a sixth field for seconds. Standard POSIX cron only understands five. Libraries like BullMQ, Quartz, and Spring accept six or even seven fields (with year).
The limitations of cron.
- No monitoring: failures go silent. Output is mailed to the user — in 2026, nobody reads root's mailbox.
- No catch-up: if the box was off when the job was supposed to run, it just doesn't (anacron solves this).
- Single-box: no distributed locking — the same cron on two boxes runs twice.
- Timezone: uses system TZ. If it isn't UTC, DST will make jobs vanish for an hour or fire twice.
- Limited expressiveness: "the last weekday of each month" is hard to express.
Yet cron lives on. Simplicity is the value. For a one-line job, on one box, where monitoring is obviously unnecessary (log rotation, temp file cleanup), cron is still optimal.
3. systemd timers — The Modern Linux Replacement
After systemd unified the init system around the mid-2010s, systemd timers became the modern alternative to cron. RHEL/CentOS Stream, Ubuntu, Debian, and Fedora all default to systemd now.
A systemd timer is two files.
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.target
Activate.
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
sudo systemctl list-timers
Why systemd timers beat cron.
- Journal logging integration:
journalctl -u backup.serviceshows execution logs. cron mails or syslogs. - Persistent: jobs missed while the box was off run once when it comes back up. anacron built in.
- RandomizedDelaySec: avoids the thundering herd of every box firing cron at exactly the same minute.
- OnCalendar expressiveness:
Mon..Fri 09:00,*-*-1..7 09:00(first week of each month, weekdays),quarterly,yearly, all human-readable. - Resource limits: the Service unit owns
CPUQuota,MemoryMax,IOSchedulingClasscgroup limits directly. - Dependencies:
Requires=,After=express "only run when another service is up" conditions.
OnCalendar examples.
# Daily at 03:00
OnCalendar=daily
# Every Sunday at 04:00
OnCalendar=Sun 04:00
# The 1st and 15th of each month
OnCalendar=*-*-01,15 02:00
# Every 10 minutes, weekdays
OnCalendar=Mon..Fri *:0/10
# Every 6 hours (0, 6, 12, 18)
OnCalendar=*-*-* 0/6:00:00
systemd-analyze calendar 'Mon..Fri *:0/10' validates the expression and prints the next firing time.
Migrating from cron to systemd timers is effectively the standard 2026 recommendation. Ubuntu 24.04 LTS and RHEL 10 both make cron an optional package and ship systemd timers as the primary mechanism.
4. Vercel Cron — Serverless Scheduling
Vercel Cron went GA in 2022 and is, by 2026, the standard scheduling mechanism integrated with Next.js, SvelteKit, and similar frameworks.
Configuration lives in one vercel.json.
{
"crons": [
{
"path": "/api/cron/daily-report",
"schedule": "0 9 * * *"
},
{
"path": "/api/cron/cleanup",
"schedule": "*/15 * * * *"
}
]
}
Handler (Next.js App Router).
// app/api/cron/daily-report/route.ts
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
// Vercel sends its own secret via Authorization
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new NextResponse('Unauthorized', { status: 401 })
}
// Real logic
await sendDailyReport()
return NextResponse.json({ ok: true })
}
Vercel Cron characteristics.
- Hobby/Free plan: up to 2 crons per day, once per minute. Learning / personal projects.
- Pro plan: 40 crons, multiple firings per minute. Production-capable.
- Enterprise: effectively unlimited.
- Timeout: must finish within the function timeout (Pro 60s, Enterprise 900s).
- HMAC auth: the CRON_SECRET env var prevents arbitrary external invocation.
- Monitoring: last execution and status visible in the Vercel dashboard.
The limitations are clear. Anything that must finish inside the function timeout works; long-running work (batch processing, large data) does not. The standard pattern is to push to a job queue (Inngest, Trigger.dev, QStash) and return immediately. Vercel Cron itself is "an alarm clock that wakes you up at the right time."
5. AWS EventBridge Scheduler (Nov 2022) — Replacing CloudWatch Events Rules
AWS's cron got a generational refresh in November 2022 with EventBridge Scheduler. The previous standard, CloudWatch Events Rules (CloudWatch Events rate / cron expressions), is now in legacy mode. EventBridge Scheduler scales to 1 million schedules per account and is the recommended option.
The differences.
| Aspect | CloudWatch Events Rules (legacy) | EventBridge Scheduler (new) |
|---|---|---|
| Launched | 2016 | November 2022 |
| Schedule limit per account | 100~300 (service quotas) | 1 million |
| One-time schedules | None | at(...) expressions |
| Timezone | UTC only | Explicit IANA TZ |
| Flexible time window | None | FlexibleTimeWindowMinutes |
| Target integrations | Via EventBus indirection | Direct to Lambda, SQS, ECS, SageMaker, Step Functions and 200+ APIs |
| Recommendation | Deprecated guidance | Official AWS recommendation |
Creating a Scheduler (Terraform).
resource "aws_scheduler_schedule" "daily_report" {
name = "daily-report"
group_name = "default"
flexible_time_window {
mode = "OFF"
}
schedule_expression = "cron(0 9 ? * MON-FRI *)"
schedule_expression_timezone = "Asia/Seoul"
target {
arn = aws_lambda_function.report.arn
role_arn = aws_iam_role.scheduler.arn
retry_policy {
maximum_retry_attempts = 3
maximum_event_age_in_seconds = 3600
}
}
}
AWS cron expressions are six fields (minute hour day month dow year), and the ? marker means "don't care about this field." Subtly different from POSIX cron — watch out.
Another option is Step Functions Wait state plus EventBridge. Patterns like "wait 5 minutes, then move to the next step" express the wait natively in Step Functions; external wake-ups use EventBridge Scheduler.
2026 recommendation: new projects use EventBridge Scheduler; migrate CloudWatch Events Rules gradually. AWS even provides a migration tool in the console.
6. Cloudflare Workers Cron Triggers
Cloudflare Workers is an edge function platform, and Cron Triggers is its cron-expression-based scheduler. As of 2026, even the free Workers plan allows up to 3 crons.
wrangler.toml configuration.
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-05-01"
[triggers]
crons = [
"0 */6 * * *",
"0 0 * * 0"
]
Handler.
// src/index.ts
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
console.log(`cron fired: ${event.cron} at ${event.scheduledTime}`)
// real work
await runCleanup(env)
},
async fetch(req: Request): Promise<Response> {
return new Response('Worker alive')
}
}
Cloudflare differentiators.
- Edge global execution: Cloudflare decides where the cron wakes up (routing optimization). If your workload has timezone dependencies, handle it in code.
- Included in free tier: 100k requests/day, with cron included.
- 30s CPU limit: standard Workers cap at 30s of CPU. Long work needs Durable Objects or Queues.
- Event metadata:
event.crontells you which expression fired, so one function can branch across multiple schedules.
Workers Cron Triggers resembles Vercel Cron but wins on edge distribution and generous free tier.
7. GitHub Actions schedule / GitLab schedule pipelines
The pattern of "CI/CD platform itself becomes a cron" exploded after 2020. Infrastructure ops, data sync, periodic checks — manage all of it through workflow files.
GitHub Actions schedule example.
# .github/workflows/nightly.yml
name: Nightly Maintenance
on:
schedule:
- cron: '0 17 * * *' # UTC 17:00 = KST 02:00
workflow_dispatch: # also allow manual
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run cleanup
run: ./scripts/cleanup.sh
GitHub Actions schedule gotchas.
- Hardcoded UTC: no timezone option. Convert yourself.
- Auto-pause for low-activity repos: after 60 days without a push, scheduled workflows auto-disable. Push a commit to revive.
- Delays: due to GitHub load, actual firing is often several to tens of minutes behind the schedule.
- Free quota: public repos free, private repos burn minute quota.
GitLab schedule pipelines.
# .gitlab-ci.yml
nightly:
script:
- ./scripts/cleanup.sh
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
GitLab schedules are configured in the CI/CD > Schedules UI with time, timezone, and variables. Unlike GitHub Actions, it supports a real timezone field.
When CI-based schedules make sense.
- Dependency security scans / SBOM refreshes.
- Static-site regenerations (prices, FX rates, weather).
- Daily/weekly report emails.
- Git mirror jobs.
When they don't.
- Anything requiring sub-minute punctuality (CI often slips behind schedule).
- Real-time infrastructure operations (EventBridge Scheduler or Vercel Cron are better).
8. Kubernetes CronJobs — kind: CronJob
CronJob stabilized in Kubernetes 1.21. It's cron syntax layered on top of the Job resource (a one-shot container).
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-cleanup
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Seoul"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: registry.example.com/cleanup:1.4.2
command: ["/app/cleanup.sh"]
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "1", memory: "512Mi" }
Key CronJob fields.
- schedule: five-field cron expression.
- timeZone: stable since 1.25. IANA TZ (e.g.,
Asia/Tokyo). - concurrencyPolicy:
Allow(default) /Forbid(don't start a new job if a previous one is running) /Replace(kill the previous). - startingDeadlineSeconds: if the controller is late, give up after this many seconds.
- successfulJobsHistoryLimit / failedJobsHistoryLimit: how many Job objects to retain.
- backoffLimit: how many Pod failures before giving up.
CronJob pitfalls.
- Clock drift: if nodes disagree on time, fire times wobble. NTP is required.
- Controller lag: kube-controller-manager under load can lag, with delays from one second to tens of seconds.
- No timeout set: without
activeDeadlineSeconds, a Job can run forever. - Timezone trap: clusters pre-1.25 are UTC-only. The classic bug: DST transitions can fire twice or zero times.
Operational tips.
- Per CronJob, define a PodDisruptionBudget and PriorityClass to stay safe across node disruptions.
- Logging: stdout/stderr go to container logs, then to Loki / OpenSearch.
- Monitoring: Prometheus
kube_cronjob_*metrics plus Alertmanager → "alert if no success in the last X hours".
9. BullMQ (Node) / Sidekiq (Ruby) / Celery Beat (Python)
In-app background jobs plus periodic work is the realm of Redis-backed job queues with a scheduler. The three big players in 2026.
BullMQ — Node.js
BullMQ is OptimalBits's successor to Bull and the de facto Node.js standard queue in 2026.
import { Queue, Worker, QueueEvents } from 'bullmq'
const connection = { host: '127.0.0.1', port: 6379 }
// Queue
const reportsQueue = new Queue('reports', { connection })
// Register a repeating (cron) job
await reportsQueue.add(
'daily-report',
{ type: 'pdf' },
{
repeat: { pattern: '0 9 * * 1-5', tz: 'Asia/Seoul' },
jobId: 'daily-report',
}
)
// Worker
new Worker('reports', async (job) => {
console.log(`processing ${job.name}`)
await generateReport(job.data)
}, { connection })
BullMQ strengths.
- TypeScript first: type-safe.
- Repeatable jobs: cron or every (ms). TZ supported.
- Flow producer: parent-child job dependencies (workflow-style).
- Rate limiting / priority / retries: one-line config.
BullMQ Pro is the commercial tier: groups, pausable groups, prioritized groups, and other enterprise features. Maintained by Taskforce.sh — the OSS half stays very active.
Sidekiq — Ruby
Sidekiq dominates Ruby. Mike Perham started it in 2012, and it's still actively maintained in 2026.
# Gemfile
gem 'sidekiq'
gem 'sidekiq-scheduler' # or sidekiq-cron
# config/sidekiq.yml
:schedule:
daily_report:
cron: '0 9 * * 1-5'
class: DailyReportJob
queue: reports
# app/jobs/daily_report_job.rb
class DailyReportJob
include Sidekiq::Job
sidekiq_options queue: :reports, retry: 3
def perform
ReportMailer.daily.deliver_now
end
end
Sidekiq variants.
- Sidekiq (OSS): free, Redis-backed, Web UI built in.
- Sidekiq Pro: paid. Reliability adds (reliable fetch, batches).
- Sidekiq Enterprise: more expensive paid. Unique jobs, native periodic jobs (no separate gem), multi-process, multi-DC.
Mike Perham's business model is a frequently cited example of a single-maintainer OSS + commercial pair.
Celery Beat — Python
Celery is the standard Python background queue. Celery Beat is its scheduler.
# celery_app.py
from celery import Celery
from celery.schedules import crontab
app = Celery('myapp', broker='redis://localhost:6379/0')
app.conf.beat_schedule = {
'daily-report': {
'task': 'tasks.generate_report',
'schedule': crontab(hour=9, minute=0, day_of_week='1-5'),
},
'every-15-min-cleanup': {
'task': 'tasks.cleanup',
'schedule': 900.0, # seconds
},
}
app.conf.timezone = 'Asia/Seoul'
# tasks.py
@app.task
def generate_report():
...
Run.
# Worker
celery -A celery_app worker -l info
# Beat scheduler (single instance only)
celery -A celery_app beat -l info
The Celery Beat trap — only one Beat node. Two Beat processes enqueue every task twice. For distributed-lock semantics, use celery-redbeat as the backend (Redis distributed lock; two Beats alive, but only one enqueues).
10. RQ / dramatiq / APScheduler — Python Options
Beyond Celery, Python has several lighter alternatives.
RQ (Redis Queue)
The simplest Python job queue. Far simpler than Celery, with proportionally fewer features.
from rq import Queue
from rq_scheduler import Scheduler
from redis import Redis
from datetime import datetime
scheduler = Scheduler(connection=Redis())
scheduler.cron(
'0 9 * * 1-5',
func=generate_report,
queue_name='reports',
)
The rq-scheduler package adds cron-expression support. RQ fits single-box, single-worker setups with lightweight background work.
dramatiq
More robust than RQ, simpler than Celery. RabbitMQ is recommended but Redis works.
import dramatiq
from dramatiq.brokers.rabbitmq import RabbitmqBroker
from dramatiq_crontab import cron
dramatiq.set_broker(RabbitmqBroker())
@cron('0 9 * * 1-5')
@dramatiq.actor
def daily_report():
...
Dramatiq's strengths: clean retry backoff, dead-letter handling, middleware system. The middle ground for teams who find Celery too heavy and RQ too light.
APScheduler
An in-process scheduler with no separate worker. Called directly from Flask or FastAPI.
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
scheduler = AsyncIOScheduler(timezone='Asia/Seoul')
scheduler.add_job(
daily_report,
CronTrigger.from_crontab('0 9 * * 1-5')
)
scheduler.start()
APScheduler fits 1-2 process small apps where bringing in Redis/RabbitMQ feels heavy. Limits: weak distributed locking, jobs lost if the process dies. SQLAlchemy / MongoDB / Redis job stores exist but none are strongly consistent.
Selection guide.
| Tool | Backend | Distributed | Complexity | Fit |
|---|---|---|---|---|
| Celery + Beat | Redis / RabbitMQ | Yes (one Beat) | High | Large apps, varied workloads |
| RQ + rq-scheduler | Redis | Medium | Low | Single-box, lightweight |
| dramatiq | RabbitMQ / Redis | Yes | Medium | Reliability + simplicity |
| APScheduler | In-process | No | Very low | 1-2 process small apps |
11. Airflow / Dagster / Prefect / Temporal / Inngest / Trigger.dev — Workflow
Workflow engines are cron's natural evolution. Multi-step + retries + state tracking + visualization. This category got a dedicated article elsewhere, so here we focus on the cron angle.
Apache Airflow
The standard for data engineering. Put schedule_interval in a DAG (Directed Acyclic Graph).
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime
with DAG(
'daily_etl',
schedule_interval='0 2 * * *',
start_date=datetime(2026, 1, 1),
catchup=False,
tags=['etl'],
) as dag:
extract = PythonOperator(task_id='extract', python_callable=do_extract)
transform = PythonOperator(task_id='transform', python_callable=do_transform)
load = PythonOperator(task_id='load', python_callable=do_load)
extract >> transform >> load
Airflow strengths: visualization and catchup (backfilling missed runs). Weaknesses: operational complexity and a heavy scheduler.
Dagster
Asset-based model. Beyond cron, it can fire when data assets become stale.
from dagster import asset, ScheduleDefinition, define_asset_job
@asset
def daily_metrics():
...
daily_job = define_asset_job('daily_metrics_job', selection=[daily_metrics])
daily_schedule = ScheduleDefinition(
job=daily_job,
cron_schedule='0 2 * * *',
execution_timezone='Asia/Seoul',
)
Prefect
Decorator-style Python workflows. Cron, interval, and rrule (recurrence) all supported.
from prefect import flow, task
from prefect.deployments import Deployment
from prefect.client.schemas.schedules import CronSchedule
@task
def fetch(): ...
@flow
def daily_pipeline():
fetch()
Deployment.build_from_flow(
flow=daily_pipeline,
name='daily',
schedule=CronSchedule(cron='0 2 * * *', timezone='Asia/Seoul'),
).apply()
Temporal Schedules
Temporal is the rising star among workflow engines. The Schedules API became GA in 2023, combining cron with workflow durability.
import { Client } from '@temporalio/client'
const client = new Client()
await client.schedule.create({
scheduleId: 'daily-report',
spec: {
cronExpressions: ['0 9 * * 1-5'],
timezoneName: 'Asia/Seoul',
},
action: {
type: 'startWorkflow',
workflowType: 'DailyReportWorkflow',
workflowId: 'daily-report',
taskQueue: 'reports',
},
policies: {
overlap: 'SKIP', // skip if the previous workflow hasn't finished
catchupWindow: '1h',
},
})
Temporal Schedules's strength: the workflow itself is durable. Cron fires, workflow starts — and if the host dies, another host continues it. True "fire and forget."
Inngest Crons
Serverless functions plus event-driven workflows. Cron alongside event triggers.
import { inngest } from './client'
export const dailyReport = inngest.createFunction(
{ id: 'daily-report' },
{ cron: 'TZ=Asia/Seoul 0 9 * * 1-5' },
async ({ step }) => {
const data = await step.run('fetch', fetchData)
await step.run('send', () => sendReport(data))
}
)
Trigger.dev v3
React-like workflow code. Beyond cron, cron.list and schedules.create() provide dynamic schedule APIs.
import { schedules } from '@trigger.dev/sdk/v3'
export const dailyReport = schedules.task({
id: 'daily-report',
cron: { pattern: '0 9 * * 1-5', timezone: 'Asia/Seoul' },
run: async (payload) => {
// payload.timestamp gives the firing timestamp
}
})
12. Hatchet — The Newcomer Workflow
Hatchet appeared in 2024. Its differentiator: PostgreSQL alone hosts job queues, workflow state, and schedules.
import { Hatchet } from '@hatchet-dev/typescript-sdk'
const hatchet = Hatchet.init()
hatchet.workflow({
id: 'daily-report',
on: { cron: '0 9 * * 1-5' },
steps: [
{
name: 'fetch-data',
run: async (ctx) => fetchData(),
},
{
name: 'send-email',
parents: ['fetch-data'],
run: async (ctx) => {
const data = ctx.stepOutput('fetch-data')
await sendEmail(data)
},
}
]
})
Hatchet differentiators.
- PostgreSQL-only dependency: no Redis, Kafka, or NATS. One Postgres instance handles queue, job storage, and workflow state.
- Self-host friendly: a single Docker image runs the full stack.
- OpenTelemetry built in: jobs and workflows produce traces out of the box.
- Free / paid: OSS free, Hatchet Cloud is the managed option.
Hatchet fits in 2026: small-to-mid teams already running PostgreSQL who want something lighter than Temporal.
13. Quartz (Java) / Spring @Scheduled / Hangfire (.NET)
Cron in the JVM and .NET worlds.
Quartz Scheduler
The classic JVM scheduling library, around since 1999, still active in 2026.
import org.quartz.*;
import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
public class DailyReportJob implements Job {
public void execute(JobExecutionContext ctx) {
// work
}
}
JobDetail job = newJob(DailyReportJob.class)
.withIdentity("dailyReport", "reports")
.build();
CronTrigger trigger = (CronTrigger) newTrigger()
.withIdentity("dailyReportTrigger", "reports")
.withSchedule(cronSchedule("0 0 9 ? * MON-FRI").inTimeZone(TimeZone.getTimeZone("Asia/Seoul")))
.build();
scheduler.scheduleJob(job, trigger);
Quartz persists job state to an RDB via JDBC JobStore and supports cluster mode (multiple scheduler instances sharing one DB). Reach for it when you need finer control than AWS EventBridge or BullMQ give you.
Spring @Scheduled
Inside Spring Boot, one annotation creates a cron job.
@Component
public class ScheduledTasks {
@Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Seoul")
public void dailyReport() {
// work
}
@Scheduled(fixedRate = 60000)
public void healthcheck() {
// every 60 seconds
}
}
Add @EnableScheduling to your main class to enable. Spring's cron is six-field (second minute hour day month dow) — different from POSIX cron, so watch out.
In a distributed environment, layer ShedLock to prevent two instances from running the same job twice.
Hangfire — .NET
.NET's background-jobs-plus-scheduler library.
// Startup.cs
services.AddHangfire(c => c.UseSqlServerStorage(connStr));
services.AddHangfireServer();
// Program / Controller
RecurringJob.AddOrUpdate<IDailyReportService>(
"daily-report",
svc => svc.SendReport(),
"0 9 * * 1-5",
TimeZoneInfo.FindSystemTimeZoneById("Korea Standard Time")
);
Hangfire strengths.
- SQL Server / PostgreSQL / Redis backends, your choice.
- Dashboard: job status, failures, retries in the browser.
- Automatic retry: exponential backoff on failure.
- OSS free + Hangfire Pro paid.
Hangfire is the de facto standard in the .NET world.
It integrates cleanly with IoC containers like Castle Windsor for tidy dependency injection.
14. fcron / anacron — Linux Alternatives
Two cron alternatives in the Linux ecosystem.
anacron
A cron complement for environments where the box isn't always running (laptops, desktops). If a job was scheduled while the host was off, it runs once when the host comes back up.
# /etc/anacrontab
# period delay job-identifier command
1 5 cron.daily run-parts /etc/cron.daily
7 25 cron.weekly run-parts /etc/cron.weekly
@monthly 45 cron.monthly run-parts /etc/cron.monthly
period is days, delay is post-boot wait minutes, and job-identifier is the name used to track last-run state. systemd timers' Persistent=true does the same thing more cleanly.
fcron
An integration attempt of cron and anacron. Active since 1999, with the goal of "one tool for varied environments."
# fcrontab example
@daily,mailto=ops backup.sh
@reboot,nice=10 startup-checks.sh
%every 10 minutes after boot * * * * /usr/local/bin/poll.sh
fcron differentiators.
- Human-readable
@daily,%hours,%minsshortcuts. - Relative time triggers like "N minutes after boot."
- Catch-up for jobs missed while the box was off.
- Resource controls like limits on concurrent job runs.
In 2026, fcron is niche. systemd timers cover essentially the same use cases with deeper system integration. fcron still sees use in some Debian / Gentoo environments.
15. Healthchecks.io / Cronitor.io — Monitoring
Dead-man's-switch services that solve cron's "silent failure" problem. The job has to ping "done" — if the ping doesn't arrive, alert. Covered in more depth elsewhere; here, just the usage pattern.
Healthchecks.io
OSS plus managed. Generous free plan (20 checks).
# crontab
0 9 * * 1-5 /usr/local/bin/report.sh && curl -fsS --retry 3 https://hc-ping.com/PROJECT_ID/daily-report > /dev/null
Or split start / success / failure pings.
#!/bin/bash
URL="https://hc-ping.com/PROJECT_ID/daily-report"
curl -fsS --retry 3 "$URL/start"
if /usr/local/bin/report.sh; then
curl -fsS --retry 3 "$URL"
else
curl -fsS --retry 3 "$URL/fail"
fi
The time between /start and the success ping is the measured job duration. /fail triggers failure alerts. Slack, email, PagerDuty integrations are built in.
Cronitor.io
Commercial SaaS. More elaborate metrics, alert routing. Healthchecks.io's managed competitor.
# Cronitor follows the same pattern
0 9 * * 1-5 cronitor exec daily-report /usr/local/bin/report.sh
The cronitor CLI emits start / success / failure pings automatically.
Operational recommendation: every important cron gets monitoring. The 2026 SRE consensus is: "a cron without a ping is operational debt."
16. Nomad periodic jobs
HashiCorp Nomad's cron equivalent. Supports containers, native binaries, Java, Docker, QEMU, and more — the variety is the differentiator.
job "daily-cleanup" {
type = "batch"
periodic {
cron = "0 2 * * *"
time_zone = "Asia/Seoul"
prohibit_overlap = true
}
group "cleanup" {
task "run" {
driver = "docker"
config {
image = "registry.example.com/cleanup:1.4.2"
command = "/app/cleanup.sh"
}
resources {
cpu = 500
memory = 256
}
}
}
}
Nomad strengths.
- Single binary: HashiCorp's standard ops model.
- Diverse drivers: not just Docker — raw_exec, java, qemu, podman, etc.
- Consul / Vault integration: service discovery and secrets.
- Simpler than k8s: easier for small teams to operate.
Compared to Kubernetes CronJob, Nomad fits small to mid environments better. For large SaaS, k8s dominates.
17. Korea / Japan — Toss, Kakao, Mercari
Toss — cron Infrastructure
Toss has given multiple SLASH conference talks on "distributed cron infrastructure." The key points.
- Internal ScheduleJob platform: Toss's in-house cron manager centrally managing thousands of cron jobs.
- Distributed locking: Redis or ZooKeeper distributed locks prevent the same job from running on two boxes.
- Abstraction layer over k8s CronJob: developers register via UI, not YAML.
- Auto-monitoring: every registered job gets an automatic dead-man's-switch.
- Timezone consolidation: every job is KST; UTC conversion is the platform's responsibility.
Toss's core insight: "when you cross a few hundred cron jobs, you don't need a tool — you need a platform."
Kakao — Task Scheduling
Kakao has shared task scheduling infrastructure at its if(kakao) conference.
- KakaoTalk push: scheduled push notifications use a dedicated scheduler, custom-built on top of Quartz.
- Data pipelines: Airflow plus an in-house wrapper.
- Ad settlement: Sidekiq-style job queue plus cron.
Kakao's lesson: "no single tool does everything. The reality is a mosaic of the best-fit tool per workload."
Mercari — Scheduled Jobs
Mercari Japan is well known as a microservices operations role model.
- Google Cloud Tasks plus Cloud Scheduler: GCP-centric infrastructure.
- k8s CronJob: infrastructure-cleanup work.
- In-process schedulers inside each microservice: lightweight jobs handled by the service itself.
The Mercari Engineering blog (English) publishes operational case studies regularly.
18. Who Should Pick What — Simple / Distributed / Background / Reliability
Putting 2026 cron selection into a decision tree.
Job runs as a single command on one box?
├── YES → systemd timers (not cron)
│ + Healthchecks.io ping
└── NO →
Serverless infra?
├── Vercel-based → Vercel Cron
├── AWS-based → EventBridge Scheduler
├── Cloudflare → Workers Cron Triggers
└── Containers → k8s CronJob or Nomad periodic
Job is part of an in-app background system?
├── Node → BullMQ
├── Ruby → Sidekiq
├── Python → Celery Beat (large) / RQ (small) / dramatiq (mid)
├── Java → Quartz / Spring @Scheduled
└── .NET → Hangfire
Job is a multi-step workflow?
├── Data → Airflow / Dagster / Prefect
├── Reliability → Temporal
├── Serverless → Inngest / Trigger.dev v3
└── Postgres-only → Hatchet
Tied to CI activity?
└── GitHub Actions schedule / GitLab schedule pipelines
Rules of thumb.
- Simple job + single box → systemd timers.
- Cloud-managed → Vercel / EventBridge / Cloudflare, depending on infra lock-in.
- App integration → language-specific standards (BullMQ / Sidekiq / Celery Beat / Quartz / Hangfire).
- Workflow reliability → Temporal / Hatchet.
- Data pipelines → Airflow / Dagster / Prefect.
- No matter what → Healthchecks.io or Cronitor.io for monitoring.
In 2026, cron operations isn't about "one tool does it all." The standard is "pick the best tool per workload, and monitor all of them."
References
- cron(8) — Unix manual page
- crontab(5) — format specification
- systemd.timer documentation
- systemd-analyze calendar
- Vercel Cron Jobs
- AWS EventBridge Scheduler announcement (Nov 2022)
- AWS EventBridge Scheduler documentation
- Cloudflare Workers Cron Triggers
- GitHub Actions schedule events
- GitLab Pipeline schedules
- Kubernetes CronJob
- BullMQ documentation
- BullMQ Pro
- Sidekiq documentation
- Sidekiq Pro / Enterprise
- Celery documentation
- Celery Beat periodic tasks
- RQ — Redis Queue
- dramatiq documentation
- APScheduler documentation
- Apache Airflow
- Dagster schedules
- Prefect schedules
- Temporal Schedules
- Inngest crons
- Trigger.dev v3 Scheduled tasks
- Hatchet documentation
- Quartz Scheduler
- Spring @Scheduled
- Hangfire documentation
- fcron homepage
- anacron documentation
- Healthchecks.io
- Cronitor
- HashiCorp Nomad periodic jobs
- Toss SLASH
- Kakao if(kakao) conference
- Mercari Engineering