Fehlerbehebung · Vercel

Vercel-Cronjob läuft nicht? Diese Punkte zuerst prüfen

Warum ein Vercel-Cronjob nicht läuft oder auslöst — nur Production-Deploys, Hobby-Plan-Limits mit Stundenfenster-Timing, CRON_SECRET-401s, maxDuration-Timeouts — und wie Sie es beheben.

Vercel-Crons sind bequem — bis sie es nicht mehr sind. Die meisten „läuft nie“-Fälle gehen auf eine von fünf Ursachen zurück — der Reihe nach.

Diagnose in 30 Sekunden:

SymptomWahrscheinlichste UrsacheAbschnitt
Läuft nie, Dashboard zeigt keinen CronConfig nicht im Production-Deploy§1
Läuft zu scheinbar zufälliger ZeitHobby-Plan-Stundenfenster§2
Läuft, liefert aber 401/403Auth-Middleware / CRON_SECRET§3
Startet, wird aber nie fertigmaxDuration-Timeout§4
Läuft zur „falschen“ StundeZeitpläne sind nur UTC§5

1. Crons laufen nur auf dem Production-Deployment

In vercel.json definierte Cronjobs werden nur für das aktuelle Production-Deployment aktiviert. Sie laufen nicht auf Preview-Deployments, und Änderungen an der crons-Konfiguration greifen erst, wenn Sie erneut nach Production deployen.

{
  "crons": [
    { "path": "/api/reports/nightly", "schedule": "0 3 * * *" }
  ]
}

Prüfen Sie das Dashboard: Project → Settings → Cron Jobs sollte den Job mit seiner nächsten geplanten Laufzeit anzeigen. Fehlt er dort, hat die Konfiguration nie ein Production-Deployment erreicht. Häufige Wege dorthin:

  • die vercel.json-Änderung wurde gemerged, aber der Production-Deploy schlug fehl oder wurde übersprungen;
  • die Datei liegt in einem Unterverzeichnis, das nicht das von Vercel gebaute Projekt-Root ist;
  • ein Tippfehler im crons-Key — unbekannte Konfiguration ignoriert Vercel stillschweigend.

Außerdem: Crons sind pausiert, solange das Projekt pausiert ist. Maßgeblich ist immer das aktuelle Production-Deployment.

2. Im Hobby-Plan ist das Timing ungefähr — absichtlich

Hobby-Plan-Crons sind auf 2 Cronjobs limitiert, die höchstens einmal täglich laufen — und der Aufruf kann irgendwann innerhalb der Stunde der geplanten Zeit erfolgen. 0 3 * * * heißt „irgendwann zwischen 03:00 und 03:59 UTC“ — nicht 03:00. Das ist dokumentiertes Verhalten, kein Bug, und überrascht regelmäßig alle, die einen „Mitternachts-Cron“ um 00:47 feuern sehen.

Zwei Folgeeffekte:

  • Ein Zeitplan häufiger als täglich (etwa */10 * * * *) ist im Hobby-Plan ungültig — das Deploy warnt, und der Cron läuft nicht wie geschrieben.
  • Weil das Fenster eine Stunde breit ist, können zwei aufeinanderfolgende Läufe ~23 bis ~25 Stunden auseinanderliegen. Alles, was „exakt 24h seit dem letzten Lauf“ annimmt, braucht Spielraum.

Für minutengenaues Timing oder untertägige Zeitpläne braucht es den Pro-Plan — oder einen externen Scheduler, der dieselbe Route zeitgenau aufruft (siehe letzter Abschnitt). Vertiefung: Vercel-Cron-Limits erklärt.

3. Die Route lehnt den Request ab (401/403)

Vercel ruft Ihren Cron als einfachen GET-Request auf. Sitzt die Route hinter Ihrer eigenen Auth-Middleware, wird der Cron wie jeder anonyme Aufrufer abgewiesen.

Das unterstützte Muster: Umgebungsvariable CRON_SECRET setzen. Vercel sendet sie als Bearer-Token mit, und Sie prüfen sie im Handler:

export function GET(req: Request) {
  const auth = req.headers.get("authorization");
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }
  // ... der eigentliche Job
}

Drei Stolperfallen dabei:

  • Nach dem Setzen oder Rotieren von CRON_SECRET neu deployen — Env-Änderungen wirken nicht auf das laufende Production-Deployment.
  • Globale Middleware (middleware.ts) läuft vor dem Handler — leitet sie unauthentifizierte Requests zur Login-Seite um, bekommt der Cron einen 307 und der Handler läuft nie. Cron-Pfad ausnehmen oder den Header in der Middleware prüfen.
  • Deployment Protection (passwort-/SSO-geschützte Deployments) blockiert Cron-Aufrufe genauso wie anonyme Besucher.

4. Die Function läuft mitten im Job in den Timeout

Ein Cron-Aufruf ist ein normaler Serverless-Function-Call mit denselben maxDuration-Limits wie jeder andere. Ein Nightly-Job, der über das Limit gewachsen ist, wird mitten im Lauf abgebrochen — der Zeitplan feuert, die Arbeit wird nicht fertig.

Prüfen Sie Logs / Observability auf Task timed out-Einträge und erhöhen Sie maxDuration in der Route-Konfiguration (Limits je nach Plan):

export const maxDuration = 300; // Sekunden — Route-Segment-Konfiguration

Braucht der Job legitim länger als das Plan-Maximum: in Batches aufteilen, oder die schwere Arbeit in eine Queue verlagern und den Cron nur enqueuen lassen.

5. Der Zeitplan ist UTC und nur 5 Felder

Keine Zeitzonen-Unterstützung, kein Sekundenfeld, keine @daily-Abkürzungen. 0 9 * * * ist ganzjährig 09:00 UTC — Ihr Berliner 9-Uhr-Job driftet mit der Sommerzeit um eine Stunde, und es gibt in Vercel keine Einstellung dagegen.

Unsicher, was ein Ausdruck tut? Der Cron-Ausdruck-Erklärer zeigt konkrete nächste Laufzeiten.

Das eigentliche Problem: keine Retries, keine Alerts

Vercel-Cron gibt Ihnen einen Trigger und ein Log — sonst nichts. Ein fehlgeschlagener Lauf wird nicht wiederholt. Niemand bekommt eine E-Mail. Ein Job, der drei Wochen lang jede Nacht mit 500 fehlschlägt, sieht exakt aus wie ein gesunder, solange niemand die Logs liest.

Zwei robuste Upgrades, mit zunehmender Kontrolle:

  1. Vercel-Cron behalten, Heartbeat ergänzen. Die letzte Zeile des Handlers pingt einen Monitor; ein ausgebliebener oder fehlgeschlagener Lauf alarmiert innerhalb Ihrer Grace-Zeit:

    await fetch("https://ping.steadycron.com/<ihr-ping-token>");
    

    Weil der Ping nur feuert, wenn der Handler fertig wird, fängt das alles oben Genannte auf einmal: fehlende Production-Deploys, 401s, Timeouts und schlichte Crashes. Einrichtung unter Heartbeat-Monitoring.

  2. Den Trigger aus Vercel herausziehen. Route behalten, crons-Konfiguration löschen und einen externen Scheduler aufrufen lassen — mit exaktem Timing, Zeitzonen pro Job, Retries mit Backoff und einem Ausführungsprotokoll jeder Response. Genau das machen SteadyCrons HTTP-Jobs; die Route bleibt durch denselben CRON_SECRET-Header geschützt, und steadycron import vercel konvertiert Ihre bestehende vercel.json in einem Befehl. Siehe Migration von Vercel-Cron.

Häufige Fragen

Warum läuft mein Vercel-Cronjob überhaupt nicht?

Meist hat die crons-Konfiguration nie ein Production-Deployment erreicht — Cronjobs werden nur auf dem aktuellen Production-Deploy aktiviert, nie auf Previews. Prüfen Sie Project → Settings → Cron Jobs: Fehlt der Job dort mit nächster Laufzeit, deployen Sie erneut nach Production.

Wie viele Cronjobs erlaubt der Vercel-Hobby-Plan?

Der Hobby-Plan erlaubt 2 Cronjobs, jeder höchstens einmal täglich, und der Aufruf landet irgendwann innerhalb der geplanten Stunde. Minutengenaues Timing und häufigere Zeitpläne erfordern den Pro-Plan — oder einen externen Scheduler, der dieselbe Route aufruft.

Warum läuft mein Vercel-Cron zu einer zufälligen Zeit innerhalb der Stunde?

Das ist dokumentiertes Hobby-Plan-Verhalten: 0 3 * * * bedeutet „irgendwann zwischen 03:00 und 03:59 UTC“, nicht Punkt 03:00. Es ist eine bewusste Lastverteilung, kein Bug.

Warum liefert mein Vercel-Cron 401 Unauthorized?

Vercel ruft Crons als einfache GET-Requests auf. Prüft Ihre Route Authentifizierung, setzen Sie die Umgebungsvariable CRON_SECRET — Vercel sendet sie als Bearer-Token — und verifizieren Sie sie im Handler. Nach dem Setzen oder Rotieren: neu deployen.

Wiederholt Vercel einen fehlgeschlagenen Cronjob oder alarmiert mich?

Nein. Ein fehlgeschlagener Lauf wird nicht wiederholt, und es gibt keine Benachrichtigung — ein Job, der jede Nacht mit 500 fehlschlägt, sieht identisch zu einem gesunden aus, solange niemand die Logs liest. Ergänzen Sie einen Heartbeat-Ping oder verlagern Sie den Trigger zu einem Scheduler mit Retries und Alerting.