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:
| Cron | Bedeutung |
|---|---|
0 2 * * * | Jeden Tag um 02:00 |
*/5 * * * * | Alle 5 Minuten |
0 0 * * 0 | Jeden 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:
- Nuxt 5 kommt
- Nitro v3 Tasks & Background Jobs (dieser Artikel)
- Nitro v3 WebSockets
- Nitro v3 Breaking Changes
- Migration Guide: Nuxt 5
Wenn du Background Jobs in Projekten umsetzt, ist die wichtigste Frage: Brauchst du Persistenz? Wenn nein, sind Nitro Tasks perfekt.