← Zurück zum Blog

Nitro v3 Tasks & Background Jobs - wie sie wirklich funktionieren (und wann du sie brauchst)

Ein typischer Moment in jedem SaaS-Projekt:

„Kann das System die Reports automatisch generieren?" „Können wir nachts die Datenbank aufräumen?" „Können Imports im Hintergrund laufen?"

Die Antwort war bisher:

„Ja… aber wir brauchen dafür noch einen extra Worker / Cron Setup / Queue Service."

Das bedeutete dann:

  • Redis
  • Bull / Agenda
  • separater Node-Prozess
  • oder externe Services wie AWS Lambda, Azure Functions, etc.

Mit Nitro v3 ändert sich das.

Nitro bringt Tasks & Cron Scheduling nativ mit.

Keine externe Infrastruktur.
Kein zusätzliches Setup.
Einfach: server/tasks/* erstellen und fertig.

In diesem Artikel schauen wir uns an:

  • Wie das neue Tasks API funktioniert
  • Wie Cron Scheduling eingebaut ist
  • Was Tasks können (und was nicht)
  • Wann du trotzdem eine Queue brauchst
  • Praktische Beispiele für echte Projekte

Das Problem: Background Jobs in Nuxt waren immer „irgendwie gelöst"

Wenn man ehrlich ist:

Background Jobs in Nuxt 3 waren möglich, aber nie elegant.

Die typischen Lösungen waren:

Variante 1: API Endpoint, der „einfach läuft"

// server/api/cleanup.ts
export default defineEventHandler(async (event) => {
  // cleanup logic here
  await cleanupOldRecords();
  return { success: true };
});

Dann manuell aufrufen oder mit einem externen Cron Job triggern.

Problem:

  • Das ist kein Background Job - das ist ein API Call
  • Wenn der Request timeoutet, bricht alles ab
  • Keine Wiederholung bei Fehlern
  • Schwer zu monitoren

Variante 2: setTimeout / setInterval

// server/plugins/scheduler.ts
export default defineNitroPlugin(() => {
  setInterval(() => {
    console.log("Running cleanup...");
    cleanupOldRecords();
  }, 60000); // every minute
});

Problem:

  • Läuft bei jedem Serverstart neu
  • Bei Serverless / Edge funktioniert das nicht
  • Keine Fehlerbehandlung
  • Kein echtes Scheduling

Variante 3: Externe Queue (Bull, Agenda, BullMQ)

import Queue from "bull";

const cleanupQueue = new Queue("cleanup", {
  redis: { host: "localhost", port: 6379 },
});

cleanupQueue.process(async (job) => {
  await cleanupOldRecords();
});

Problem:

  • Redis Dependency
  • extra Service
  • komplexeres Deployment
  • Overkill für viele Projekte

Die Realität war:

Viele Projekte haben Background Jobs entweder:

  • gar nicht gemacht
  • oder „irgendwie gehackt"
  • oder mit viel Overhead implementiert

Nitro v3 Tasks: Die native Lösung

Mit Nitro v3 kommt ein neues Konzept:

Tasks in server/tasks/*

Tasks sind:

  • eigenständige Einheiten (keine API Routes)
  • können manuell getriggert werden
  • können geplant werden (Cron)
  • laufen im Hintergrund
  • haben eigene Fehlerbehandlung

Wie ein Task aussieht

// server/tasks/cleanup.ts
export default defineTask({
  meta: {
    name: "cleanup",
    description: "Clean up old records from database",
  },
  run: async ({ payload, context }) => {
    console.log("Starting cleanup task...");

    const deletedCount = await cleanupOldRecords();

    return {
      result: `Deleted ${deletedCount} records`,
    };
  },
});

Das ist alles.

Kein Redis.
Kein Bull.
Kein extra Service.

Tasks manuell triggern

Du kannst Tasks von überall triggern:

// server/api/trigger-cleanup.ts
export default defineEventHandler(async (event) => {
  const result = await runTask("cleanup");

  return {
    success: true,
    result,
  };
});

Oder von anderen Tasks:

// server/tasks/daily-report.ts
export default defineTask({
  meta: {
    name: "daily-report",
  },
  run: async () => {
    // Generate report
    await generateReport();

    // Then trigger cleanup
    await runTask("cleanup");

    return { success: true };
  },
});

Tasks mit Payload

Tasks können Parameter bekommen:

// server/tasks/send-email.ts
export default defineTask({
  meta: {
    name: "send-email",
  },
  run: async ({ payload }) => {
    const { to, subject, body } = payload;

    await sendEmail(to, subject, body);

    return { sent: true };
  },
});

Und triggern:

await runTask("send-email", {
  payload: {
    to: "user@example.com",
    subject: "Welcome!",
    body: "Thanks for signing up.",
  },
});

Cron Scheduling: Tasks automatisch ausführen

Jetzt wird es richtig spannend.

Tasks alleine sind cool - aber Cron Scheduling macht sie wirklich mächtig.

Wie Cron Scheduling funktioniert

Du kannst in der Task-Definition einfach einen Cron-Ausdruck hinterlegen:

// server/tasks/nightly-backup.ts
export default defineTask({
  meta: {
    name: "nightly-backup",
    description: "Backup database every night at 2 AM",
  },
  schedule: "0 2 * * *", // Cron expression
  run: async () => {
    console.log("Starting nightly backup...");

    await backupDatabase();

    return { success: true };
  },
});

Das war's.

Kein externer Cron.
Kein separater Scheduler.
Einfach: schedule: '0 2 * * *'

Cron Expressions (Quick Reference)

Falls du Cron Expressions nicht im Kopf hast:

┌───────────── minute (0 - 59)
│ ┌─────────── hour (0 - 23)
│ │ ┌───────── day of month (1 - 31)
│ │ │ ┌─────── month (1 - 12)
│ │ │ │ ┌───── day of week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
* * * * *

Typische Beispiele:

CronBedeutung
0 2 * * *Jeden Tag um 02:00
*/5 * * * *Alle 5 Minuten
0 0 * * 0Jeden Sonntag um Mitternacht
0 9 1 * *Jeden 1. des Monats um 09:00
0 */6 * * *Alle 6 Stunden

Mehrere geplante Tasks

Du kannst beliebig viele Tasks mit Scheduling haben:

// server/tasks/cache-cleanup.ts
export default defineTask({
  meta: { name: "cache-cleanup" },
  schedule: "*/10 * * * *", // every 10 minutes
  run: async () => {
    await cleanupExpiredCache();
    return { success: true };
  },
});

// server/tasks/send-reports.ts
export default defineTask({
  meta: { name: "send-reports" },
  schedule: "0 8 * * 1", // every Monday at 8 AM
  run: async () => {
    await sendWeeklyReports();
    return { success: true };
  },
});

// server/tasks/database-optimization.ts
export default defineTask({
  meta: { name: "database-optimization" },
  schedule: "0 3 * * 0", // every Sunday at 3 AM
  run: async () => {
    await optimizeDatabase();
    return { success: true };
  },
});

Das ist genau das, was man in SaaS-Projekten braucht.

Praktische Use Cases (aus echten Projekten)

Jetzt wird es konkret.

Hier sind typische Szenarien, wo Tasks & Cron richtig Sinn machen:

1) Datenbank Cleanup

// server/tasks/cleanup-old-sessions.ts
export default defineTask({
  meta: {
    name: "cleanup-old-sessions",
    description: "Remove expired sessions older than 30 days",
  },
  schedule: "0 3 * * *", // every day at 3 AM
  run: async () => {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - 30);

    const result = await db.sessions.deleteMany({
      where: {
        expiresAt: { lt: cutoffDate },
      },
    });

    return {
      deleted: result.count,
      cutoffDate: cutoffDate.toISOString(),
    };
  },
});

Warum das wichtig ist:

In echten Projekten sammeln sich Daten an - Sessions, Logs, temporäre Files.

Ohne automatisches Cleanup wird die Datenbank immer langsamer.

2) Report Generierung

// server/tasks/generate-monthly-reports.ts
export default defineTask({
  meta: {
    name: "generate-monthly-reports",
  },
  schedule: "0 6 1 * *", // 1st of every month at 6 AM
  run: async () => {
    const lastMonth = new Date();
    lastMonth.setMonth(lastMonth.getMonth() - 1);

    // Generate report
    const report = await generateSalesReport(lastMonth);

    // Store in database
    await db.reports.create({
      data: {
        type: "monthly-sales",
        period: lastMonth,
        data: report,
      },
    });

    // Send to admins
    await runTask("send-email", {
      payload: {
        to: "admin@company.com",
        subject: "Monthly Report Ready",
        body: "The monthly sales report has been generated.",
      },
    });

    return { success: true };
  },
});

3) External API Sync

// server/tasks/sync-business-central.ts
export default defineTask({
  meta: {
    name: "sync-business-central",
    description: "Sync items from Business Central ERP",
  },
  schedule: "0 * * * *", // every hour
  run: async () => {
    const bcClient = await getBusinessCentralClient();

    // Fetch items from BC API
    const items = await bcClient.get("/items");

    // Update local database
    for (const item of items.value) {
      await db.items.upsert({
        where: { bcId: item.id },
        update: {
          name: item.displayName,
          price: item.unitPrice,
          stock: item.inventory,
        },
        create: {
          bcId: item.id,
          name: item.displayName,
          price: item.unitPrice,
          stock: item.inventory,
        },
      });
    }

    return {
      synced: items.value.length,
      timestamp: new Date().toISOString(),
    };
  },
});

Das ist ein typisches Szenario in Business-Anwendungen:

Daten aus einem ERP-System synchronisieren - ohne dass User manuell triggern muss.

💡 Seitennotiz:
Wenn du OAuth für Business Central nutzt, schau dir OAuth in Business Central an.

4) Cache Warmup

// server/tasks/warmup-cache.ts
export default defineTask({
  meta: {
    name: "warmup-cache",
  },
  schedule: "0 */4 * * *", // every 4 hours
  run: async () => {
    // Pre-fetch expensive data
    const products = await fetchAllProducts();
    const categories = await fetchCategories();

    // Store in cache
    await redis.set("products:all", JSON.stringify(products), "EX", 14400);
    await redis.set("categories:all", JSON.stringify(categories), "EX", 14400);

    return {
      cached: {
        products: products.length,
        categories: categories.length,
      },
    };
  },
});

5) Long-running Imports

// server/tasks/import-csv.ts
export default defineTask({
  meta: {
    name: "import-csv",
  },
  run: async ({ payload }) => {
    const { fileUrl, userId } = payload;

    // Download CSV
    const csv = await downloadFile(fileUrl);

    // Parse
    const records = parseCSV(csv);

    // Import in batches
    let imported = 0;
    for (const batch of chunk(records, 100)) {
      await db.records.createMany({
        data: batch.map((r) => ({
          ...r,
          userId,
        })),
      });
      imported += batch.length;
    }

    // Notify user
    await runTask("send-email", {
      payload: {
        to: getUserEmail(userId),
        subject: "Import Complete",
        body: `Successfully imported ${imported} records.`,
      },
    });

    return { imported };
  },
});

Dann triggern z. B. aus einem API Endpoint:

// server/api/import.ts
export default defineEventHandler(async (event) => {
  const { fileUrl } = await readBody(event);
  const userId = event.context.user.id;

  // Trigger task (runs in background)
  runTask("import-csv", {
    payload: { fileUrl, userId },
  });

  return {
    message: "Import started. You will receive an email when done.",
  };
});

Das ist der richtige Weg für Long-Running Tasks:

User bekommt sofort Response.
Task läuft im Hintergrund.
User wird benachrichtigt, wenn fertig.

Was Tasks NICHT können (wichtig!)

Jetzt kommt der Reality-Check.

Nitro Tasks sind cool - aber sie sind kein Ersatz für alles.

❌ Keine Persistenz bei Server-Restart

Wenn dein Server neu startet:

  • geplante Tasks laufen weiter
  • aber laufende Tasks brechen ab

Das ist ein Problem, wenn:

  • Tasks lange laufen (> 30 Sekunden)
  • Server oft neu starten (z. B. bei Deployments)

Lösung:

Für kritische, lange laufende Tasks brauchst du trotzdem eine Queue mit Persistenz (Bull, BullMQ, etc.).

❌ Keine Retry-Logic (out of the box)

Wenn ein Task fehlschlägt:

  • wird er NICHT automatisch wiederholt
  • du musst Retry-Logic selbst bauen

Lösung:

export default defineTask({
  meta: { name: "flaky-api-call" },
  run: async () => {
    let attempts = 0;
    const maxAttempts = 3;

    while (attempts < maxAttempts) {
      try {
        const result = await callFlakyAPI();
        return { success: true, result };
      } catch (error) {
        attempts++;
        if (attempts >= maxAttempts) throw error;
        await sleep(1000 * attempts); // exponential backoff
      }
    }
  },
});

Oder: Bull/BullMQ nutzen (die haben Retry eingebaut).

❌ Keine Concurrency-Control

Wenn du den gleichen Task mehrfach triggerst:

  • laufen alle parallel
  • keine automatische Lock-Mechanik

Problem:

// server/api/trigger-sync.ts
export default defineEventHandler(async () => {
  // If multiple users hit this endpoint:
  await runTask("sync-business-central"); // runs multiple times!
});

Lösung:

Du musst selbst Locks implementieren (z. B. mit Redis oder DB-basiert).

❌ Keine Priority Queues

Tasks haben keine Priorität.

Du kannst nicht sagen:

„Task A ist wichtiger als Task B."

Lösung:

Wenn du das brauchst: Bull/BullMQ oder ähnliche Queue-Systeme.

❌ Kein Multi-Server Clustering (out of the box)

Wenn du mehrere Server-Instanzen hast:

  • läuft jeder Cron-Task auf jeder Instanz

Das bedeutet:

Wenn du 3 Server hast und schedule: '0 2 * * *' definierst, läuft der Task 3x.

Lösung:

Du brauchst entweder:

  • einen dedizierten „Task Runner" Server
  • oder eine Queue mit Redis Lock
  • oder eine Plattform mit built-in Task Scheduling (z. B. Vercel Cron, Cloudflare Workers Cron)

Wann du trotzdem eine Queue brauchst

Tasks & Cron sind perfekt für:

  • einfache Background Jobs
  • geplante Aufgaben
  • kleine bis mittelgroße Projekte
  • wenn du keine extra Infrastruktur willst

Aber:

Wenn dein Projekt folgendes braucht:

  • Persistenz (Tasks müssen überleben, wenn Server neu startet)
  • Retry-Logic (automatisch wiederholen bei Fehler)
  • Priority Queues (wichtige Tasks zuerst)
  • Concurrency Control (max X Tasks parallel)
  • Multi-Server Clustering (nur eine Instanz führt Task aus)
  • Job History (welche Tasks sind wann gelaufen)

… dann brauchst du eine echte Queue:

  • Bull / BullMQ
  • Agenda
  • oder managed Services wie AWS SQS, Azure Queue Storage

Best Practices

✅ Tasks immer idempotent machen

Idempotent = Task kann mehrfach ausgeführt werden, ohne Probleme zu verursachen.

Schlecht:

export default defineTask({
  run: async () => {
    // BAD: creates duplicate records if run twice
    await db.users.create({
      data: { email: "admin@example.com" },
    });
  },
});

Gut:

export default defineTask({
  run: async () => {
    // GOOD: upsert ensures no duplicates
    await db.users.upsert({
      where: { email: "admin@example.com" },
      update: {},
      create: { email: "admin@example.com" },
    });
  },
});

✅ Fehlerbehandlung einbauen

export default defineTask({
  run: async () => {
    try {
      await riskyOperation();
      return { success: true };
    } catch (error) {
      console.error("Task failed:", error);
      // Optional: send alert
      await sendErrorAlert(error);
      throw error;
    }
  },
});

✅ Logging für Monitoring

export default defineTask({
  meta: { name: "important-sync" },
  run: async () => {
    const startTime = Date.now();

    console.log("[TASK] important-sync started");

    const result = await syncData();

    const duration = Date.now() - startTime;
    console.log(`[TASK] important-sync completed in ${duration}ms`);

    return { result, duration };
  },
});

✅ Timeouts setzen (wenn möglich)

export default defineTask({
  run: async () => {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 30000); // 30s

    try {
      const result = await fetch("https://api.example.com", {
        signal: controller.signal,
      });
      clearTimeout(timeout);
      return result;
    } catch (error) {
      if (error.name === "AbortError") {
        console.error("Task timed out");
      }
      throw error;
    }
  },
});

✅ Environment-spezifische Tasks

Manchmal willst du Tasks nur in Production:

export default defineTask({
  meta: { name: "production-backup" },
  schedule: process.env.NODE_ENV === "production" ? "0 2 * * *" : undefined,
  run: async () => {
    if (process.env.NODE_ENV !== "production") {
      console.log("Skipping backup in non-production");
      return { skipped: true };
    }

    await backupDatabase();
    return { success: true };
  },
});

Fazit

Nitro v3 Tasks & Cron Scheduling sind ein Gamechanger für Nuxt-Projekte.

Sie lösen das Problem, das viele hatten:

„Wie mache ich Background Jobs, ohne extra Infrastruktur?"

Die Antwort ist jetzt:

Einfach server/tasks/* erstellen.

Was Tasks können:

  • Background Jobs
  • Cron Scheduling
  • manuelles Triggering
  • Payload-Support
  • saubere Architektur

Was Tasks NICHT können:

  • Persistenz bei Server-Restart
  • automatische Retries
  • Priority Queues
  • Multi-Server Clustering (out of the box)

Fazit:

Für 80% der Projekte sind Tasks & Cron mehr als genug.

Für die anderen 20% brauchst du trotzdem eine Queue.

Aber:

Weniger externe Abhängigkeiten ist immer gut.

Und genau das liefert Nitro v3.


Die Artikel-Serie:

  1. Nuxt 5 kommt
  2. Nitro v3 Tasks & Background Jobs (dieser Artikel)
  3. Nitro v3 WebSockets
  4. Nitro v3 Breaking Changes
  5. Migration Guide: Nuxt 5

Wenn du Background Jobs in Projekten umsetzt, ist die wichtigste Frage: Brauchst du Persistenz? Wenn nein, sind Nitro Tasks perfekt.