Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
515 lines
13 KiB
Markdown
515 lines
13 KiB
Markdown
# Cron Jobs & Task Scheduling in Node.js
|
||
|
||
**Meta-Description:** Task Scheduling für Node.js. Cron Syntax, node-cron, BullMQ Repeatable Jobs und Serverless Scheduling.
|
||
|
||
**Keywords:** Cron Jobs, Task Scheduling, Node.js, node-cron, BullMQ, Scheduled Tasks, Background Jobs
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
Automatisierte Aufgaben sind das Rückgrat jeder Anwendung: **Backups, Reports, Cleanup, Sync-Jobs**. Mit Cron Jobs laufen sie zuverlässig zu definierten Zeiten – ohne manuellen Eingriff.
|
||
|
||
---
|
||
|
||
## Cron Syntax
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ CRON EXPRESSION │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌───────────── Minute (0-59) │
|
||
│ │ ┌───────────── Stunde (0-23) │
|
||
│ │ │ ┌───────────── Tag des Monats (1-31) │
|
||
│ │ │ │ ┌───────────── Monat (1-12) │
|
||
│ │ │ │ │ ┌───────────── Wochentag (0-7, 0 und 7 = Sonntag)│
|
||
│ │ │ │ │ │ │
|
||
│ * * * * * │
|
||
│ │
|
||
│ Beispiele: │
|
||
│ ├── * * * * * → Jede Minute │
|
||
│ ├── 0 * * * * → Jede volle Stunde │
|
||
│ ├── 0 0 * * * → Täglich um Mitternacht │
|
||
│ ├── 0 8 * * 1-5 → Mo-Fr um 8:00 │
|
||
│ ├── 0 0 1 * * → Am 1. jeden Monats │
|
||
│ ├── */15 * * * * → Alle 15 Minuten │
|
||
│ └── 0 9,18 * * * → Um 9:00 und 18:00 │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## node-cron Setup
|
||
|
||
```bash
|
||
npm install node-cron
|
||
npm install -D @types/node-cron
|
||
```
|
||
|
||
```typescript
|
||
import cron from 'node-cron';
|
||
|
||
// Basic Scheduled Task
|
||
cron.schedule('* * * * *', () => {
|
||
console.log('Runs every minute');
|
||
});
|
||
|
||
// Mit Timezone
|
||
cron.schedule('0 8 * * *', () => {
|
||
console.log('Daily at 8:00 AM Berlin time');
|
||
}, {
|
||
timezone: 'Europe/Berlin'
|
||
});
|
||
|
||
// Task mit Start/Stop
|
||
const task = cron.schedule('*/5 * * * *', () => {
|
||
console.log('Every 5 minutes');
|
||
}, {
|
||
scheduled: false // Nicht automatisch starten
|
||
});
|
||
|
||
// Manuell starten/stoppen
|
||
task.start();
|
||
// task.stop();
|
||
|
||
// Validation
|
||
const isValid = cron.validate('* * * * *'); // true
|
||
```
|
||
|
||
---
|
||
|
||
## Praktische Scheduler-Klasse
|
||
|
||
```typescript
|
||
import cron, { ScheduledTask } from 'node-cron';
|
||
|
||
interface Job {
|
||
name: string;
|
||
schedule: string;
|
||
handler: () => Promise<void>;
|
||
enabled: boolean;
|
||
timezone?: string;
|
||
}
|
||
|
||
class TaskScheduler {
|
||
private tasks: Map<string, ScheduledTask> = new Map();
|
||
private jobs: Map<string, Job> = new Map();
|
||
|
||
register(job: Job) {
|
||
this.jobs.set(job.name, job);
|
||
|
||
if (job.enabled) {
|
||
this.startJob(job.name);
|
||
}
|
||
}
|
||
|
||
startJob(name: string) {
|
||
const job = this.jobs.get(name);
|
||
if (!job) throw new Error(`Job ${name} not found`);
|
||
|
||
if (this.tasks.has(name)) {
|
||
console.log(`Job ${name} already running`);
|
||
return;
|
||
}
|
||
|
||
const task = cron.schedule(job.schedule, async () => {
|
||
const startTime = Date.now();
|
||
console.log(`[${name}] Starting...`);
|
||
|
||
try {
|
||
await job.handler();
|
||
console.log(`[${name}] Completed in ${Date.now() - startTime}ms`);
|
||
} catch (error) {
|
||
console.error(`[${name}] Failed:`, error);
|
||
// Alert senden
|
||
}
|
||
}, {
|
||
timezone: job.timezone || 'Europe/Berlin'
|
||
});
|
||
|
||
this.tasks.set(name, task);
|
||
console.log(`[${name}] Scheduled: ${job.schedule}`);
|
||
}
|
||
|
||
stopJob(name: string) {
|
||
const task = this.tasks.get(name);
|
||
if (task) {
|
||
task.stop();
|
||
this.tasks.delete(name);
|
||
console.log(`[${name}] Stopped`);
|
||
}
|
||
}
|
||
|
||
stopAll() {
|
||
for (const [name, task] of this.tasks) {
|
||
task.stop();
|
||
console.log(`[${name}] Stopped`);
|
||
}
|
||
this.tasks.clear();
|
||
}
|
||
|
||
getStatus(): Array<{ name: string; schedule: string; running: boolean }> {
|
||
return Array.from(this.jobs.values()).map(job => ({
|
||
name: job.name,
|
||
schedule: job.schedule,
|
||
running: this.tasks.has(job.name)
|
||
}));
|
||
}
|
||
}
|
||
|
||
// Verwendung
|
||
const scheduler = new TaskScheduler();
|
||
|
||
scheduler.register({
|
||
name: 'daily-backup',
|
||
schedule: '0 2 * * *',
|
||
handler: async () => {
|
||
await performDatabaseBackup();
|
||
},
|
||
enabled: true
|
||
});
|
||
|
||
scheduler.register({
|
||
name: 'hourly-sync',
|
||
schedule: '0 * * * *',
|
||
handler: async () => {
|
||
await syncExternalData();
|
||
},
|
||
enabled: true
|
||
});
|
||
|
||
scheduler.register({
|
||
name: 'weekly-report',
|
||
schedule: '0 9 * * 1', // Montag 9:00
|
||
handler: async () => {
|
||
await generateWeeklyReport();
|
||
await sendReportEmail();
|
||
},
|
||
enabled: true
|
||
});
|
||
|
||
// Graceful Shutdown
|
||
process.on('SIGTERM', () => {
|
||
scheduler.stopAll();
|
||
process.exit(0);
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## BullMQ Repeatable Jobs
|
||
|
||
```typescript
|
||
import { Queue, Worker } from 'bullmq';
|
||
import IORedis from 'ioredis';
|
||
|
||
const connection = new IORedis(process.env.REDIS_URL!);
|
||
|
||
// Queue für Scheduled Jobs
|
||
const scheduledQueue = new Queue('scheduled-tasks', { connection });
|
||
|
||
// Repeatable Jobs registrieren
|
||
async function setupScheduledJobs() {
|
||
// Täglich um 2:00 UTC
|
||
await scheduledQueue.add('database-backup', {}, {
|
||
repeat: {
|
||
pattern: '0 2 * * *',
|
||
tz: 'Europe/Berlin'
|
||
},
|
||
jobId: 'daily-backup' // Verhindert Duplikate
|
||
});
|
||
|
||
// Alle 5 Minuten
|
||
await scheduledQueue.add('health-check', {}, {
|
||
repeat: {
|
||
every: 5 * 60 * 1000 // Millisekunden
|
||
},
|
||
jobId: 'health-check'
|
||
});
|
||
|
||
// Wöchentlich Montag 9:00
|
||
await scheduledQueue.add('weekly-report', { type: 'weekly' }, {
|
||
repeat: {
|
||
pattern: '0 9 * * 1',
|
||
tz: 'Europe/Berlin'
|
||
},
|
||
jobId: 'weekly-report'
|
||
});
|
||
|
||
// Monatlich am 1.
|
||
await scheduledQueue.add('monthly-cleanup', {}, {
|
||
repeat: {
|
||
pattern: '0 3 1 * *',
|
||
tz: 'Europe/Berlin'
|
||
},
|
||
jobId: 'monthly-cleanup'
|
||
});
|
||
}
|
||
|
||
// Worker für Scheduled Jobs
|
||
const worker = new Worker('scheduled-tasks', async (job) => {
|
||
console.log(`Processing ${job.name}`);
|
||
|
||
switch (job.name) {
|
||
case 'database-backup':
|
||
await performDatabaseBackup();
|
||
break;
|
||
|
||
case 'health-check':
|
||
await runHealthChecks();
|
||
break;
|
||
|
||
case 'weekly-report':
|
||
await generateAndSendReport('weekly');
|
||
break;
|
||
|
||
case 'monthly-cleanup':
|
||
await cleanupOldData();
|
||
break;
|
||
|
||
default:
|
||
console.warn(`Unknown job: ${job.name}`);
|
||
}
|
||
|
||
return { completed: true };
|
||
}, { connection });
|
||
|
||
worker.on('completed', (job, result) => {
|
||
console.log(`Job ${job.name} completed`);
|
||
});
|
||
|
||
worker.on('failed', (job, error) => {
|
||
console.error(`Job ${job?.name} failed:`, error);
|
||
// Alert senden
|
||
});
|
||
|
||
// Repeatable Jobs verwalten
|
||
async function listScheduledJobs() {
|
||
const jobs = await scheduledQueue.getRepeatableJobs();
|
||
return jobs.map(job => ({
|
||
name: job.name,
|
||
pattern: job.pattern || `every ${job.every}ms`,
|
||
next: new Date(job.next).toISOString()
|
||
}));
|
||
}
|
||
|
||
async function removeScheduledJob(jobId: string) {
|
||
const jobs = await scheduledQueue.getRepeatableJobs();
|
||
const job = jobs.find(j => j.id === jobId);
|
||
|
||
if (job) {
|
||
await scheduledQueue.removeRepeatableByKey(job.key);
|
||
console.log(`Removed job: ${jobId}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Serverless Cron (Vercel/AWS)
|
||
|
||
```typescript
|
||
// Vercel Cron Jobs (vercel.json)
|
||
{
|
||
"crons": [
|
||
{
|
||
"path": "/api/cron/daily-backup",
|
||
"schedule": "0 2 * * *"
|
||
},
|
||
{
|
||
"path": "/api/cron/hourly-sync",
|
||
"schedule": "0 * * * *"
|
||
}
|
||
]
|
||
}
|
||
|
||
// app/api/cron/daily-backup/route.ts
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
|
||
export const runtime = 'edge';
|
||
export const maxDuration = 60; // 60 Sekunden max
|
||
|
||
export async function GET(request: NextRequest) {
|
||
// Verify Cron Secret (Security)
|
||
const authHeader = request.headers.get('authorization');
|
||
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||
}
|
||
|
||
try {
|
||
await performBackup();
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
} catch (error) {
|
||
console.error('Backup failed:', error);
|
||
|
||
return NextResponse.json({
|
||
success: false,
|
||
error: (error as Error).message
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
// AWS EventBridge + Lambda
|
||
// serverless.yml
|
||
// functions:
|
||
// dailyBackup:
|
||
// handler: src/handlers/backup.handler
|
||
// events:
|
||
// - schedule: cron(0 2 * * ? *)
|
||
```
|
||
|
||
---
|
||
|
||
## Job Locking (Prevent Duplicates)
|
||
|
||
```typescript
|
||
import { Redis } from 'ioredis';
|
||
|
||
const redis = new Redis(process.env.REDIS_URL!);
|
||
|
||
async function withJobLock<T>(
|
||
jobName: string,
|
||
ttlSeconds: number,
|
||
handler: () => Promise<T>
|
||
): Promise<T | null> {
|
||
const lockKey = `job-lock:${jobName}`;
|
||
|
||
// Versuche Lock zu erwerben
|
||
const acquired = await redis.set(lockKey, Date.now().toString(), 'EX', ttlSeconds, 'NX');
|
||
|
||
if (!acquired) {
|
||
console.log(`Job ${jobName} already running, skipping`);
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
return await handler();
|
||
} finally {
|
||
// Lock freigeben
|
||
await redis.del(lockKey);
|
||
}
|
||
}
|
||
|
||
// Verwendung
|
||
cron.schedule('*/5 * * * *', async () => {
|
||
await withJobLock('sync-products', 300, async () => {
|
||
// Dieser Code läuft garantiert nur einmal
|
||
await syncProducts();
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Monitoring & Alerting
|
||
|
||
```typescript
|
||
interface JobExecution {
|
||
jobName: string;
|
||
startedAt: Date;
|
||
completedAt?: Date;
|
||
status: 'running' | 'completed' | 'failed';
|
||
error?: string;
|
||
duration?: number;
|
||
}
|
||
|
||
class JobMonitor {
|
||
private executions: Map<string, JobExecution> = new Map();
|
||
|
||
async trackExecution<T>(
|
||
jobName: string,
|
||
handler: () => Promise<T>
|
||
): Promise<T> {
|
||
const executionId = `${jobName}-${Date.now()}`;
|
||
|
||
this.executions.set(executionId, {
|
||
jobName,
|
||
startedAt: new Date(),
|
||
status: 'running'
|
||
});
|
||
|
||
try {
|
||
const result = await handler();
|
||
|
||
const execution = this.executions.get(executionId)!;
|
||
execution.completedAt = new Date();
|
||
execution.status = 'completed';
|
||
execution.duration = execution.completedAt.getTime() - execution.startedAt.getTime();
|
||
|
||
// Log to database
|
||
await this.saveExecution(execution);
|
||
|
||
return result;
|
||
} catch (error) {
|
||
const execution = this.executions.get(executionId)!;
|
||
execution.completedAt = new Date();
|
||
execution.status = 'failed';
|
||
execution.error = (error as Error).message;
|
||
execution.duration = execution.completedAt.getTime() - execution.startedAt.getTime();
|
||
|
||
await this.saveExecution(execution);
|
||
await this.sendAlert(execution);
|
||
|
||
throw error;
|
||
} finally {
|
||
// Cleanup alte Executions
|
||
setTimeout(() => this.executions.delete(executionId), 60000);
|
||
}
|
||
}
|
||
|
||
private async saveExecution(execution: JobExecution) {
|
||
await db.jobExecution.create({ data: execution });
|
||
}
|
||
|
||
private async sendAlert(execution: JobExecution) {
|
||
// Slack, Email, PagerDuty, etc.
|
||
await slack.send({
|
||
text: `❌ Job Failed: ${execution.jobName}`,
|
||
attachments: [{
|
||
color: 'danger',
|
||
fields: [
|
||
{ title: 'Error', value: execution.error || 'Unknown' },
|
||
{ title: 'Duration', value: `${execution.duration}ms` }
|
||
]
|
||
}]
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Task Scheduling in Node.js:
|
||
|
||
1. **node-cron**: Einfach für Single-Server
|
||
2. **BullMQ**: Distributed, Reliable, Scalable
|
||
3. **Serverless Cron**: Vercel, AWS EventBridge
|
||
4. **Job Locking**: Verhindert Duplikate
|
||
|
||
Zuverlässige Automation für jede Anwendung.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Clock with automated tasks running at specific times, scheduling concept"
|
||
2. "Multiple servers coordinating scheduled jobs, distributed cron"
|
||
3. "Calendar with recurring events and checkmarks, automated workflow"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [node-cron Documentation](https://www.npmjs.com/package/node-cron)
|
||
- [BullMQ Repeatable Jobs](https://docs.bullmq.io/guide/jobs/repeatable)
|
||
- [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs)
|
||
- [Cron Expression Generator](https://crontab.guru/)
|