---
name: cron-scheduler-expert
description: "Expert guide for scheduled tasks, cron jobs, recurring background work (Vercel Cron, Cloudflare Workers Cron, Inngest, node-cron), and distributed scheduling / Panduan ahli untuk tugas terjadwal, cron job, pekerjaan latar belakang berulang, dan penjadwalan terdistribusi."
author: "Roedy Rustam"
version: "4.4.2"
---
# Cron & Scheduler Expert (2026 Edition)
[English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
---
## English
### Orchestration & Integration
- **Upstream Triggers & Workflows**: `async-queue-temporal-expert`, `event-driven-architect`, `js-backend-expert`
- **Cloud & Edge Platforms**: `cloud-hosting-expert`, `cloudflare-workers-expert`
- **Domain Consumer Jobs**: `saas-billing`, `email-notification-expert`, `data-pipeline-etl-expert`
- **Locking & Observability**: `database-orm-expert`, `data-telemetry-expert`, `logging-error-tracking-expert`
- **Lifecycle & Governance**: `zero-to-prod-orchestrator`, `session-memory-manager`
### Description
Production-grade guide for implementing scheduled tasks, cron jobs, and recurring background work in modern web applications. Covers **Vercel Cron Jobs**, **Cloudflare Workers Cron Triggers**, **Inngest scheduled functions**, **node-cron**, **BullMQ repeatable jobs**, **distributed cron locking** (Redis-based), **timezone-aware scheduling**, and common use cases (cleanup jobs, report generation, health checks, subscription renewals).
### Trigger Conditions
Activate this skill when:
- Setting up scheduled/recurring tasks (daily reports, cleanup, health checks).
- Configuring Vercel Cron Jobs or Cloudflare Workers Cron Triggers.
- Implementing subscription renewal checks or billing cycle jobs.
- Building data aggregation or ETL pipelines on a schedule.
- Needing distributed cron locking to prevent duplicate execution.
- Scheduling email digests or notification batches.
---
### Platform Selection Guide
| Platform | Best For | Max Frequency | Invocation Model |
|---|---|---|---|
| **Vercel Cron** | Vercel-hosted Next.js apps | 1/min (Pro), 1/day (Hobby) | HTTP endpoint trigger |
| **Cloudflare Workers Cron** | Edge-first apps | 1/min | Worker script trigger |
| **Inngest** | Complex workflows, fan-out | Any (event-driven) | Serverless function |
| **BullMQ Repeatable** | Self-hosted, Redis-backed | Any | Worker process |
| **node-cron** | Simple Node.js servers | Any | In-process |
| **Trigger.dev v3** | Durable scheduled tasks | Any | Serverless function |
**Recommendation**: Use **Vercel Cron** for Next.js apps on Vercel. Use **Inngest** for complex multi-step scheduled workflows. Use **BullMQ repeatable jobs** for self-hosted environments.
---
### 1. Vercel Cron Jobs
```json
// vercel.json
{
"crons": [
{
"path": "/api/cron/daily-cleanup",
"schedule": "0 3 * * *"
},
{
"path": "/api/cron/hourly-health-check",
"schedule": "0 * * * *"
},
{
"path": "/api/cron/weekly-report",
"schedule": "0 9 * * 1"
}
]
}
```
```typescript
// app/api/cron/daily-cleanup/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// Verify the request is from Vercel Cron (not public)
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
// Cleanup expired sessions
const deletedSessions = await db.delete(sessions)
.where(lt(sessions.expiresAt, new Date()));
// Cleanup orphaned uploads (older than 24h, not linked to any record)
const deletedUploads = await db.delete(uploads)
.where(and(
isNull(uploads.linkedRecordId),
lt(uploads.createdAt, new Date(Date.now() - 86400000)),
));
return NextResponse.json({
ok: true,
cleaned: { sessions: deletedSessions.rowCount, uploads: deletedUploads.rowCount },
});
} catch (error) {
console.error('Daily cleanup failed:', error);
return NextResponse.json({ ok: false, error: 'Cleanup failed' }, { status: 500 });
}
}
export const runtime = 'nodejs';
export const maxDuration = 60; // Allow up to 60s for cleanup
```
---
### 2. Inngest Scheduled Functions
```typescript
// inngest/functions/weekly-report.ts
import { inngest } from '@/lib/inngest';
export const weeklyReportJob = inngest.createFunction(
{
id: 'weekly-usage-report',
name: 'Generate Weekly Usage Report',
},
{ cron: 'TZ=Asia/Jakarta 0 9 * * 1' }, // Every Monday 9 AM WIB
async ({ step }) => {
// Step 1: Gather data
const metrics = await step.run('gather-metrics', async () => {
return await db.query.usageMetrics.findMany({
where: gte(usageMetrics.createdAt, subDays(new Date(), 7)),
});
});
// Step 2: Generate report
const report = await step.run('generate-report', async () => {
return generateWeeklyReport(metrics);
});
// Step 3: Send to admins
await step.run('send-report', async () => {
const admins = await db.query.users.findMany({
where: eq(users.role, 'admin'),
});
for (const admin of admins) {
await emailQueue.add('weekly-report', { to: admin.email, report });
}
});
return { sent: true, reportId: report.id };
},
);
```
---
### 3. BullMQ Repeatable Jobs
```typescript
// queues/scheduled-jobs.ts
import { Queue, Worker } from 'bullmq';
const scheduledQueue = new Queue('scheduled', {
connection: { host: process.env.REDIS_HOST },
});
// Add repeatable jobs on startup
export async function registerScheduledJobs() {
// Daily cleanup at 3 AM
await scheduledQueue.add('daily-cleanup', {}, {
repeat: { pattern: '0 3 * * *', tz: 'Asia/Jakarta' },
jobId: 'daily-cleanup', // Prevent duplicates on restart
});
// Every 5 minutes: health check
await scheduledQueue.add('health-check', {}, {
repeat: { every: 5 * 60 * 1000 }, // 5 minutes in ms
jobId: 'health-check',
});
// Monthly: subscription renewal check (1st of each month at midnight)
await scheduledQueue.add('subscription-renewal', {}, {
repeat: { pattern: '0 0 1 * *', tz: 'Asia/Jakarta' },
jobId: 'subscription-renewal',
});
}
// Worker
const worker = new Worker('scheduled', async (job) => {
switch (job.name) {
case 'daily-cleanup':
await performDailyCleanup();
break;
case 'health-check':
await performHealthCheck();
break;
case 'subscription-renewal':
await checkSubscriptionRenewals();
break;
}
}, { connection: { host: process.env.REDIS_HOST } });
```
---
### 4. Distributed Cron Locking (Redis)
```typescript
// lib/cron-lock.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
/**
* Acquire a distributed lock to prevent duplicate cron execution
* across multiple server instances.
*/
export async function acquireCronLock(
jobName: string,
ttlSeconds: number = 300,
): Promise {
const lockKey = `cron-lock:${jobName}`;
// SET NX (only if not exists) with TTL
const result = await redis.set(lockKey, Date.now().toString(), 'EX', ttlSeconds, 'NX');
return result === 'OK';
}
export async function releaseCronLock(jobName: string): Promise {
await redis.del(`cron-lock:${jobName}`);
}
// Usage in cron handler
export async function GET(request: Request) {
const locked = await acquireCronLock('daily-cleanup', 300);
if (!locked) {
return Response.json({ skipped: true, reason: 'Already running on another instance' });
}
try {
await performDailyCleanup();
return Response.json({ ok: true });
} finally {
await releaseCronLock('daily-cleanup');
}
}
```
---
### Common Cron Patterns
| Schedule | Cron Expression | Description |
|---|---|---|
| Every minute | `* * * * *` | Health checks, queue monitoring |
| Every 5 minutes | `*/5 * * * *` | Cache warming, metric aggregation |
| Every hour | `0 * * * *` | Data sync, report generation |
| Daily at 3 AM | `0 3 * * *` | Cleanup, backups |
| Weekly Monday 9 AM | `0 9 * * 1` | Weekly reports |
| Monthly 1st at midnight | `0 0 1 * *` | Billing cycles, subscription renewals |
| Weekdays at 8 AM | `0 8 * * 1-5` | Business notifications |
---
### Common Pitfalls to Avoid
| Anti-Pattern | Problem | Correct Approach |
|---|---|---|
| No distributed locking | Duplicate execution across instances | Use Redis-based NX lock |
| Long-running cron without timeout | Resource exhaustion, overlapping runs | Set `maxDuration` and use lock TTL |
| No monitoring of cron failures | Silent failures go unnoticed | Log results, alert on failures |
| Hardcoded timezone | Jobs run at wrong time after DST | Always use explicit `tz` parameter |
| Cron job does too much work | Timeout, partial completion | Break into smaller steps (Inngest/Temporal) |
### Sovereign Execution, Token Efficiency & Invariants
| Dimension | Sovereign Mandate & Deterministic Contract |
| :--- | :--- |
| **Frontier Model Cascade** | Medium / High (`GPT-5.6 Sol Medium`, `Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Grok 4.7 High Fast`) |
| **Availability Fallback** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Zero unhandled missing-model errors) |
| **KV-Cache & Token Protocol** | Pin static instructions & schemas at prompt prefix (`kv-cache-prefix-optimizer`); pass surgical `CONTEXT_MAP.md` slices (<250 tokens/call) during Swarm Fan-Out |
| **SaaT CLI & MCP Dispatch** | CLI: `vibes skill exec cron-scheduler-expert ` / MCP: `vibes_skill_dispatch`, `vibes_ast_query`, `vibes_compile_cache`, `vibes_optimize_budget`, `vibes_verify_gate` |
| **NFR & Anti-Slop Invariants** | P99 API Gateway Latency < 85ms, OpenAPI 3.1 / TypeSpec strict schema parity, RFC 9457 Problem Details error contracts, Idempotency-Key enforcement; 100% complete zero-placeholder output verified via `node scripts/check-anti-slop.js --strict` |
---
---
## Bahasa Indonesia
### Integrasi Orkestrasi
- **Alur Kerja & Pemicu Hulu**: `async-queue-temporal-expert`, `event-driven-architect`, `js-backend-expert`
- **Platform Cloud & Edge**: `cloud-hosting-expert`, `cloudflare-workers-expert`
- **Tugas Konsumen Domain**: `saas-billing`, `email-notification-expert`, `data-pipeline-etl-expert`
- **Penguncian & Observabilitas**: `database-orm-expert`, `data-telemetry-expert`, `logging-error-tracking-expert`
- **Siklus Hidup & Tata Kelola**: `zero-to-prod-orchestrator`, `session-memory-manager`
### Deskripsi
Panduan tingkat produksi untuk mengimplementasikan tugas terjadwal, cron job, dan pekerjaan latar belakang berulang di aplikasi web modern. Mencakup **Vercel Cron Jobs**, **Cloudflare Workers Cron Triggers**, **Inngest scheduled functions**, **BullMQ repeatable jobs**, **distributed cron locking** (berbasis Redis), **penjadwalan sadar zona waktu**, dan kasus penggunaan umum (pembersihan data, pembuatan laporan, health check, perpanjangan langganan).
### Kondisi Pemicu
Aktifkan skill ini ketika:
- Menyiapkan tugas terjadwal/berulang (laporan harian, pembersihan, health check).
- Mengonfigurasi Vercel Cron Jobs atau Cloudflare Workers Cron Triggers.
- Mengimplementasikan pemeriksaan perpanjangan langganan atau siklus billing.
- Membangun pipeline agregasi data atau ETL pada jadwal tertentu.
- Membutuhkan penguncian cron terdistribusi untuk mencegah eksekusi duplikat.
---
### Panduan Pemilihan Solusi Cron
| Platform | Pilihan Terbaik Untuk | Frekuensi Maksimum | Model Pemicu |
|---|---|---|---|
| **Vercel Cron** | Aplikasi Next.js di Vercel | 1x/menit (Pro), 1x/hari (Hobby) | Pemicu HTTP endpoint internal |
| **Cloudflare Workers Cron** | Arsitektur Edge murni | 1x/menit | Scheduled event script worker |
| **Inngest** | Alur bertahap (*step-functions*) multi-langkah | Fleksibel & event-driven | Serverless durable execution |
| **BullMQ Repeatable** | Aplikasi mandiri berbasis Redis | Fleksibel | Antrean worker Node.js terpisah |
---
### Implementasi Pola Kunci (TypeScript)
#### 1. Vercel Cron dengan Proteksi Autentikasi Rahasia (`CRON_SECRET`)
```json
// vercel.json
{
"crons": [
{
"path": "/api/cron/pembersihan-harian",
"schedule": "0 3 * * *"
}
]
}
```
```typescript
// app/api/cron/pembersihan-harian/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// Verifikasi permintaan resmi berasal dari scheduler Vercel
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Tidak memiliki otorisasi' }, { status: 401 });
}
try {
// Jalankan operasi pembersihan sesi kadaluarsa
const hasil = await db.delete(sesiPengguna)
.where(lt(sesiPengguna.kadaluarsaPada, new Date()));
return NextResponse.json({ ok: true, terhapus: hasil.rowCount });
} catch (error) {
console.error('Pembersihan cron gagal:', error);
return NextResponse.json({ ok: false, error: 'Gagal membersihkan data' }, { status: 500 });
}
}
export const runtime = 'nodejs';
export const maxDuration = 60; // Izinkan eksekusi hingga 60 detik
```
#### 2. Inngest Scheduled Functions Sadar Zona Waktu (`TZ=Asia/Jakarta`)
```typescript
import { inngest } from '@/lib/inngest';
export const jobLaporanMingguan = inngest.createFunction(
{ id: 'laporan-mingguan-admin', name: 'Kirim Laporan Penggunaan Mingguan' },
{ cron: 'TZ=Asia/Jakarta 0 9 * * 1' }, // Setiap Senin pukul 09:00 WIB
async ({ step }) => {
const dataMetrik = await step.run('ambil-data-metrik', async () => {
return await kumpulkanMetrik7HariTerakhir();
});
const fileLaporan = await step.run('buat-dokumen-laporan', async () => {
return await generateReportPdf(dataMetrik);
});
await step.run('kirimkan-email-admin', async () => {
await emailQueue.add('kirim-laporan', { file: fileLaporan });
});
return { status: 'sukses', terkirim: true };
}
);
```
#### 3. Distributed Cron Locking Berbasis Redis (`SET NX`)
```typescript
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
export async function dapatkanKunciCron(namaTugas: string, durasiDetik: number = 300): Promise {
const kunci = `kunci-cron:${namaTugas}`;
const status = await redis.set(kunci, Date.now().toString(), 'EX', durasiDetik, 'NX');
return status === 'OK';
}
export async function lepaskanKunciCron(namaTugas: string): Promise {
await redis.del(`kunci-cron:${namaTugas}`);
}
```
### Praktik Terbaik Keamanan Cron
- **Gunakan Distributed Lock**: Mencegah eksekusi ganda ketika aplikasi berjalan pada banyak instans server (cluster/autoscaling).
- **Tentukan `maxDuration`**: Hindari cron menggantung tanpa henti yang menghabiskan alokasi sumber daya komputasi.
- **Zona Waktu Eksplisit**: Selalu sertakan parameter timezone (`TZ=Asia/Jakarta`) agar jadwal tidak bergeser saat Daylight Saving Time (DST) di cloud server.
### Matriks Eksekusi Berdaulat, Efisiensi Token & Invarian
| Dimensi | Mandat Berdaulat & Kontrak Deterministik |
| :--- | :--- |
| **Kaskade Model Frontier** | Medium / High (`GPT-5.6 Sol Medium`, `Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Grok 4.7 High Fast`) |
| **Invarian Fallback Ketersediaan** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Bebas gagal saat model belum tersedia/kuota penuh) |
| **Protokol Hemat Token & KV-Cache** | Kunci instruksi & skema statis di prefix awal (`kv-cache-prefix-optimizer`); kirim potongan `CONTEXT_MAP.md` (<250 token/panggilan) saat Fan-Out |
| **Eksekusi SaaT CLI & MCP** | CLI: `vibes skill exec cron-scheduler-expert ` / MCP: `vibes_skill_dispatch`, `vibes_ast_query`, `vibes_compile_cache`, `vibes_optimize_budget`, `vibes_verify_gate` |
| **Invarian NFR & Anti-Slop** | Latensi P99 API < 85ms, paritas skema ketat OpenAPI 3.1 / TypeSpec, kontrak error RFC 9457 Problem Details, penegakan Idempotency-Key; kode 100% tuntas tanpa placeholder diverifikasi melalui `node scripts/check-anti-slop.js --strict` |