8.3 KiB
8.3 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-cron-scheduler-patterns | Cron / Scheduler — Quartz / cron / Inngest / Trigger.dev | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Cron / Scheduler
"매 X 시간 작업 실행". Cron 가 simple, BullMQ / Inngest / Trigger.dev / Temporal 가 modern. Distributed lock + retry + observability.
📖 핵심 개념
- Cron expression:
0 * * * *. - Single instance vs distributed.
- Missed run 처리.
- Retry / dedup / idempotency.
💻 코드 패턴
Linux cron (가장 simple)
# crontab -e
0 2 * * * /usr/bin/node /app/cleanup.js >> /var/log/cleanup.log 2>&1
→ 매일 2 AM. 1 server 만.
Cron expression
* * * * *
| | | | |
| | | | +-- Day of week (0-6, Sun=0)
| | | +---- Month (1-12)
| | +------ Day of month (1-31)
| +-------- Hour (0-23)
+---------- Minute (0-59)
0 * * * * 매 hour
*/5 * * * * 매 5 min
0 9 * * 1-5 평일 9 AM
0 0 1 * * 매월 1일 자정
→ crontab.guru 가 검증.
Node-cron (in-process)
import cron from 'node-cron';
cron.schedule('0 * * * *', async () => {
await cleanupOldData();
});
→ Single instance only. Restart 시 잃음.
BullMQ (Redis-backed)
import { Queue, Worker } from 'bullmq';
const queue = new Queue('jobs', { connection: { host: 'redis' } });
// Repeating job
await queue.add('cleanup', null, {
repeat: { pattern: '0 * * * *' },
});
// Worker
new Worker('jobs', async (job) => {
if (job.name === 'cleanup') await cleanup();
}, { connection });
→ Multi-instance OK (Redis 가 lock). Persistent.
Distributed cron 의 문제
2 instance × 같은 cron = 2번 실행.
해결:
1. Lock (Redis / DB).
2. Single leader.
3. Job queue.
Distributed lock (Redis)
async function withLock(key: string, ttl: number, fn: () => Promise<void>) {
const got = await redis.set(`lock:${key}`, 'taken', 'EX', ttl, 'NX');
if (!got) return; // 다른 instance
try { await fn(); } finally { await redis.del(`lock:${key}`); }
}
cron.schedule('0 * * * *', async () => {
await withLock('cleanup', 3600, async () => {
await cleanup();
});
});
Inngest (modern, function-as-a-service)
import { Inngest } from 'inngest';
const inngest = new Inngest({ name: 'My App' });
export const dailyReport = inngest.createFunction(
{ id: 'daily-report' },
{ cron: '0 9 * * *' },
async ({ event, step }) => {
const data = await step.run('fetch', () => fetchData());
await step.run('email', () => sendEmail(data));
}
);
→ Step = retry + dedup automatic.
Trigger.dev (alternative)
import { client } from './trigger';
client.defineJob({
id: 'daily-report',
trigger: cronTrigger({ cron: '0 9 * * *' }),
run: async (payload, io) => {
const data = await io.runTask('fetch', async () => fetchData());
await io.sendEvent('email', { data });
},
});
Temporal (workflow engine)
// Schedule
import { Connection, ScheduleClient } from '@temporalio/client';
const client = new ScheduleClient({ connection });
await client.create({
scheduleId: 'daily-cleanup',
spec: { intervals: [{ every: '1 day' }] },
action: {
type: 'startWorkflow',
workflowType: 'cleanupWorkflow',
},
});
→ Cron + workflow + retry.
Sidekiq (Ruby)
# Sidekiq scheduler
Sidekiq.configure_server do |config|
config.on(:startup) do
Sidekiq.schedule = YAML.load_file('config/schedule.yml')
Sidekiq::Scheduler.reload_schedule!
end
end
# schedule.yml
daily_report:
cron: '0 9 * * *'
class: ReportWorker
AWS EventBridge + Lambda
# Serverless
functions:
daily:
handler: handler.daily
events:
- schedule:
rate: cron(0 9 * * ? *)
→ Managed cron. Lambda 친화.
GCP Cloud Scheduler
gcloud scheduler jobs create http daily-report \
--schedule "0 9 * * *" \
--uri https://api.example.com/cron/daily \
--http-method POST
→ HTTP trigger.
Cloudflare Workers Cron
# wrangler.toml
[triggers]
crons = ["0 9 * * *", "*/5 * * * *"]
export default {
async scheduled(event, env, ctx) {
if (event.cron === '0 9 * * *') await dailyReport();
if (event.cron === '*/5 * * * *') await checkHealth();
},
};
→ Edge cron.
Vercel Cron
// vercel.json
{
"crons": [
{ "path": "/api/cron", "schedule": "0 9 * * *" }
]
}
// app/api/cron/route.ts
export async function GET(request) {
const auth = request.headers.get('authorization');
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
await dailyReport();
return Response.json({ ok: true });
}
Idempotency
async function cleanup() {
// 매 run 가 idempotent — retry safe.
await db.exec('DELETE FROM logs WHERE created_at < NOW() - INTERVAL 30 DAY');
}
→ "다시 실행해도 OK".
Missed run
Server down 1 hour → 1 cron 가 missed.
처리:
1. 무시 (간단).
2. Catch up (다음 run 가 이전 work 도).
3. Manual trigger.
→ Inngest / Temporal 가 missed-run 처리.
Backfill
// 옛 날짜 cron 재실행
for (const date of getDateRange('2026-04-01', '2026-04-30')) {
await dailyReport({ date });
}
Timezone
0 9 * * *
# Server timezone (UTC).
// Asia/Seoul = UTC+9
// 9 AM KST = 0 AM UTC
'0 0 * * *' in UTC = 9 AM KST.
→ Inngest / Trigger.dev 가 timezone 옵션.
Frequency limit
✓ 매일, 매 시간: 일반.
✗ 매 분: high-volume — queue 더 좋음.
✗ 매 초: bad pattern.
→ "매 분 미만" = queue / streaming.
Job 의 visibility
- 매 run 의 status (running / done / failed)
- Duration / retry count
- Last successful run
- Next scheduled
→ Inngest / Trigger.dev / Temporal UI.
DB 의 schedule (own)
CREATE TABLE scheduled_jobs (
id TEXT PRIMARY KEY,
cron TEXT,
last_run TIMESTAMP,
next_run TIMESTAMP,
status TEXT
);
-- 매 분 worker
SELECT * FROM scheduled_jobs WHERE next_run <= NOW();
-- → 매 job 실행, last_run / next_run update.
→ DIY 의 simple version.
Retry policy
// BullMQ
queue.add('cleanup', null, {
repeat: { pattern: '0 * * * *' },
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
Dependent cron
dailyReport 가 dailyClean 후.
→ Workflow (Temporal / Inngest) 가 좋음.
순서 + dependency + retry 가 자동.
Cron alert
- 매 run 의 metric (Prometheus).
- "Last run > expected interval" alert.
- Failure count threshold.
→ Datadog / Grafana.
Cost
Lambda + EventBridge: $ 작음 ($0.01 / month / cron).
Inngest: $0 (1k step / month free).
Trigger.dev: 비슷.
Vercel: 무료 (Pro 가 1 cron job).
Self-host: server cost.
→ Cron 가 비싼 보다 missing run 가 비싼.
When?
Simple (1 server): node-cron.
Multi-instance: BullMQ / Redis lock.
Modern serverless: Inngest / Trigger.dev / Vercel Cron.
Workflow: Temporal.
Cloud: AWS EventBridge / GCP Scheduler.
Edge: Cloudflare Cron.
Real-world
Examples:
- Daily report email (9 AM)
- Weekly digest
- Monthly billing
- Cleanup old data (1 AM)
- Health check (매 5 min)
- Sync (매 hour)
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 1 server | node-cron / Linux cron |
| Multi-instance | BullMQ / Redis lock |
| Modern serverless | Inngest / Trigger.dev |
| Workflow | Temporal |
| AWS | EventBridge + Lambda |
| Vercel | Vercel Cron |
| Edge | Cloudflare Cron |
| 매우 frequent | Queue / streaming |
❌ 안티패턴
- No lock (multi-instance): 중복.
- No idempotency: retry = corrupt.
- No alert: silent miss.
- 매 1 sec cron: queue 사용.
- Timezone confusion: 9 AM ≠ 9 AM.
- No visibility: blind.
- Missed run 무시: data 잃음.
🤖 LLM 활용 힌트
- 단순 = node-cron / Linux.
- Modern = Inngest / Trigger.dev (function-as-a-service).
- Multi-instance = BullMQ + lock.
- Workflow = Temporal.