Fehlerbehebung · Laravel

Laravel-Scheduler läuft nicht — oder suchen Sie eine Alternative?

Warum Laravels Scheduler still ausfällt — der fehlende Cron-Eintrag, falscher Benutzer, Umgebung und überlappende Tasks — und wie Sie es beheben oder ersetzen.

Laravels Scheduler wird von einem einzigen System-Cron-Eintrag angetrieben. Ist der falsch — oder die Umgebung darunter — fallen alle geplanten Tasks still aus. Hier die Checkliste.

1. Der eine Cron-Eintrag muss existieren

Laravel braucht genau eine Crontab-Zeile, die schedule:run jede Minute aufruft. Stellen Sie sicher, dass sie für den richtigen Benutzer existiert (meist Ihr Deploy-Benutzer, nicht root):

crontab -l

Sie sollte lauten:

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Fehlt sie, fügen Sie sie mit crontab -e hinzu. Ein häufiger Deploy-Fehler ist, sie als root hinzuzufügen, während die App als www-data läuft (oder umgekehrt).

2. Nutzen Sie das richtige PHP-Binary und absolute Pfade

Unter Cron lässt sich php womöglich nicht auflösen oder ist die falsche Version. Nutzen Sie den vollen Pfad und den Projektpfad:

* * * * * cd /var/www/app && /usr/bin/php8.3 artisan schedule:run >> /dev/null 2>&1

3. Die Umgebung unterscheidet sich von Ihrer Shell

Cron lädt Ihr Shell-Profil nicht, also fehlt alles, was Sie dort setzen. Laravel liest .env, was in Ordnung ist — aber wenn Ihre Tasks andere Binaries aufrufen, geben Sie ihnen einen expliziten PATH. Stellen Sie außerdem sicher, dass APP_ENV sowie Queue-/Cache-Konfiguration zur Produktion passen.

4. Sie haben die Ausgabe unterdrückt und können nun nicht debuggen

>> /dev/null 2>&1 verbirgt alles, auch Fehler. Loggen Sie es vorübergehend:

* * * * * cd /var/www/app && php artisan schedule:run >> storage/logs/schedule.log 2>&1

Führen Sie dann php artisan schedule:run von Hand aus und lesen Sie die Ausgabe — die meisten Fehler (Berechtigungen, fehlende Env, DB-Verbindung) zeigen sich sofort.

5. Tasks überlappen oder hängen

Ein langer Task, der jede Minute läuft, kann sich stauen. Nutzen Sie Laravels Guards, damit ein langsamer Lauf den nächsten nicht blockiert:

$schedule->command('reports:build')
    ->hourly()
    ->withoutOverlapping()
    ->onOneServer();

Das tiefere Problem: Der Scheduler kann stoppen und still bleiben

Stoppt schedule:run — der Server wurde neu gestartet, die Crontab beim Deploy gelöscht, PHP aktualisiert — hat Laravel keine Möglichkeit, es Ihnen zu sagen. Ihre in Queues eingereihten Reports und E-Mails hören einfach auf.

Pingen Sie einen Heartbeat aus einem geplanten Task, damit Sie wissen, dass der Scheduler selbst lebt:

$schedule->call(function () {
    Http::timeout(10)->get('https://ping.steadycron.com/<ihr-ping-token>');
})->everyFifteenMinutes();

Bleibt dieser Ping aus, alarmiert Sie SteadyCron — der Scheduler ist down, bevor Ihre Nutzer es merken.

Suchen Sie eine Laravel-Scheduler-Alternative ganz ohne Cron?

Wenn Sie es leid sind, immer wieder dieselbe Art von Problem zu beheben — eine gelöschte Crontab, eine Server-Migration, die genau die eine Zeile verliert, von der alles abhängt, ein schedule:run, das technisch feuert, aber still einen Fehler wirft — müssen Sie diesen Single Point of Failure gar nicht erst behalten.

Die meisten $schedule->command(...)- und $schedule->call(...)-Einträge lassen sich als Route abbilden:

// routes/api.php
Route::post('/cron/build-reports', function () {
    Artisan::call('reports:build');
    return response()->json(['ok' => true]);
})->middleware('auth.cron-token');

SteadyCron ruft diesen Endpunkt dann planmäßig auf — mit eigenem Retry/Backoff, Timeout und Ausführungsprotokoll — statt darauf zu vertrauen, dass ein System-Cron-Eintrag existiert, das richtige PHP-Binary stimmt oder schedule:run überhaupt aufgerufen wird. Der Zeitplan lebt jetzt in SteadyCron, überlebt Server-Migrationen unverändert, und jeder Lauf zeigt Statuscode und Antwort — nicht nur das, was es in storage/logs geschafft hat.

Dieselbe Idee wird ausführlicher auf der Seite Docker-Cron-Alternative behandelt — den Trigger aus dem Prozess herauszunehmen, der eigentlich nur Requests bedienen soll.