Menu
Nazad na Blog
1 min read
KI-Entwicklung

Cron Jobs & Task Scheduling in Node.js

Task Scheduling für Node.js. Cron Syntax, node-cron, BullMQ Repeatable Jobs und Serverless Scheduling.

Cron JobsTask SchedulingNode.jsnode-cronBullMQScheduled Tasks
Cron Jobs & Task Scheduling in Node.js

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

npm install node-cron
npm install -D @types/node-cron
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

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

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)

// 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)

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

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