Fehlerbehebung · GitHub Actions

GitHub-Actions-Cron-Schedule läuft nicht? Alle stillen Ursachen

Warum ein GitHub-Actions-Cron-Schedule nicht läuft oder auslöst — falscher Branch, 60-Tage-Auto-Deaktivierung, nur UTC, Lastverzögerungen, verworfene Läufe, Ausgabenlimits — und die Lösung für jede Ursache.

on: schedule ist der trügerischste Trigger in GitHub Actions. Er scheitert aus Gründen, die nirgendwo einen Fehler erzeugen — kein rotes X, keine E-Mail, kein Log. Diese Anleitung geht alle bekannten Ursachen durch, grob nach Wahrscheinlichkeit sortiert.

Diagnose in 30 Sekunden:

SymptomWahrscheinlichste UrsacheAbschnitt
Lief noch nie, kein einziges MalDatei nicht auf dem Default-Branch§1
Lief wochen-/monatelang, dann Stopp60-Tage-Deaktivierung (öffentliche Repos)§2
Läuft, aber 10–40 Minuten zu spätBest-Effort-Warteschlange§3
Läuft zur „falschen“ ZeitCron ist immer UTC§4
Überspringt gelegentlich einen LaufUnter Last verworfen§3
Stopp nach einer Billing-ÄnderungAusgabenlimit (private Repos)§5

1. Liegt die Workflow-Datei auf dem Default-Branch?

Geplante Workflows laufen nur vom Default-Branch (meist main). Ein Workflow auf einem Feature-Branch — oder gemerged in irgendetwas anderes als den Default-Branch — löst nie aus, egal wie korrekt der Cron-Ausdruck ist. Das heißt auch: Ein Schedule, an dem Sie in einem PR arbeiten, läuft erst nach dem Merge.

# die Datei muss hier existieren:
git show origin/main:.github/workflows/nightly.yml

Zum Testen auf einem Branch ergänzen Sie neben dem Schedule einen workflow_dispatch:-Trigger für manuelle Läufe:

on:
  schedule:
    - cron: "17 3 * * *"
  workflow_dispatch: {}     # manuelle Läufe während der Entwicklung

Eine weitere Feinheit: Es läuft der Schedule, der auf dem Default-Branch definiert ist. Den Cron-Ausdruck auf einem Feature-Branch zu ändern bewirkt bis zum Merge nichts.

2. Hat GitHub nach 60 Tagen Inaktivität deaktiviert?

In öffentlichen Repositories deaktiviert GitHub geplante Workflows automatisch, wenn das Repository 60 Tage keine Aktivität hatte. Das ist der Klassiker „lief monatelang, hörte dann still auf“ — und trifft überproportional genau die Repos, die auf Schedules angewiesen sind: Scraper, Backup-Jobs, Daten-Pipelines ohne regelmäßige Commits.

Prüfen Sie den Actions-Tab → Workflow auswählen. Wurde er deaktiviert, sehen Sie ein Banner: „This scheduled workflow is disabled because there hasn’t been activity in this repository for at least 60 days“ — mit einem Enable workflow-Button. GitHub verschickt vor der Deaktivierung auch eine E-Mail an den Autor, die aber leicht untergeht.

Reaktivieren behebt es bis zu den nächsten 60 ruhigen Tagen. Für ein von Natur aus ruhiges Repo gibt es drei dauerhafte Optionen:

  1. Echte Aktivität — jeder Commit setzt die Uhr zurück.
  2. Ein Keep-Alive-Step — manche Workflows committen eine Timestamp-Datei oder nutzen eine Keep-Alive-Action. Funktioniert, verschmutzt aber die Historie, nur um den Scheduler zufriedenzustellen.
  3. Den Trigger aus GitHub herausziehen — den Job als workflow_dispatch behalten und von einem externen Scheduler per GitHub-API auslösen lassen (siehe letzter Abschnitt). Externe API-Trigger zählen als Aktivität und sind zeitgenau.

3. Mit Verzögerungen rechnen — und mit verworfenen Läufen

GitHub führt geplante Workflows in einer Best-Effort-Warteschlange aus, und die offizielle Dokumentation sagt ausdrücklich, dass geplante Events bei hoher Last verzögert und verzögerte Läufe komplett verworfen werden können. Zwei praktische Konsequenzen:

  • Verzögerungen von 5–30+ Minuten sind normal, am schlimmsten zur vollen Stunde, wenn sich alle 0 * * * *-Jobs stapeln. Planen Sie auf eine ungerade Minute (17 * * * *, 43 3 * * *), um dem Andrang auszuweichen — das nimmt zuverlässig die Spitze, beseitigt die Verzögerung aber nicht.
  • Unter hoher Last werden Läufe ohne Retry und ohne Benachrichtigung übersprungen. Wenn Ihr Job „alles seit dem letzten Lauf“ verarbeitet, schreiben Sie ihn tolerant gegenüber fehlenden Intervallen (das Fenster vom letzten erfolgreichen Lauf ableiten, nicht von „jetzt minus eine Stunde“).

Außerdem gilt die Untergrenze: Schedules laufen höchstens alle 5 Minuten, und selbst die landen in der Praxis unregelmäßig.

Ihre tatsächliche Verzögerung können Sie messen — created_at des Laufs mit der geplanten Minute vergleichen:

gh run list --workflow nightly.yml --json createdAt,displayTitle --limit 10

Wenn Ihr Anwendungsfall minutengenaue Ausführung braucht (Abrechnungen, SLA-Reports, zeitkritische Benachrichtigungen), ist GitHubs Scheduler das falsche Werkzeug — der GitHub-Actions-Cron-Vergleich zeigt, was ein zeitgenauer Scheduler ändert.

4. Der Cron-Ausdruck ist UTC — immer

GitHub Actions hat keine Zeitzonen-Einstellung für Schedules. 0 9 * * * ist 09:00 UTC:

Sie schreibenUTCBerlin (Winter)Berlin (Sommer)
0 9 * * *09:0010:0011:00
30 4 * * 1Mo 04:30Mo 05:30Mo 06:30

Ihr „9-Uhr-Job“ verschiebt sich lokal zweimal im Jahr um eine Stunde. Innerhalb von GitHub gibt es keinen Workaround, außer zwei Workflows zu pflegen und an den Zeitumstellungen umzuschalten — oder extern von einem zeitzonenbewussten Scheduler zu triggern.

Der Ausdruck selbst ist Standard-Cron mit 5 Feldern (kein Sekundenfeld, keine @daily-Aliasse). Anführungszeichen beibehalten — ein nacktes * kann das YAML-Parsing stolpern lassen:

on:
  schedule:
    - cron: "*/15 * * * *"   # alle 15 Minuten (UTC)
    - cron: "0 3 * * 1-5"    # 03:00 UTC an Wochentagen — mehrere Einträge sind ok

Unsicher, was ein Ausdruck bedeutet? Der Cron-Ausdruck-Erklärer übersetzt ihn in Klartext.

5. Weitere stille Kill-Switches

  • Manuell deaktiviert — jemand hat im Actions-Tab „Disable workflow“ geklickt. Die Datei existiert weiter, alles sieht normal aus, nichts läuft.
  • Ausgabenlimit erreicht — bei privaten Repos stoppen alle Läufe, wenn das Actions-Minutenbudget erschöpft ist oder eine Zahlung fehlschlug.
  • Forks — geplante Workflows sind in Forks standardmäßig deaktiviert.
  • Repository archiviert — Archivieren stoppt alle Workflows.
  • Organisations- oder Enterprise-Richtlinie — Admins können Actions deaktivieren, auf ausgewählte Repos beschränken oder benötigte Third-Party-Actions blockieren.
  • Der vorherige Lauf läuft noch — kombiniert mit Concurrency-Regeln können „fehlende“ Läufe in Wahrheit übersprungene sein.

Der schnelle Weg, den echten Zustand per API zu prüfen:

gh api repos/OWNER/REPO/actions/workflows --jq '.workflows[] | {name, state, path}'

state zeigt disabled_inactivity, disabled_manually oder active — und unterscheidet §2 von §5 sofort.

Das eigentliche Problem: Niemand sagt Ihnen, wenn der Schedule stoppt

Jede Fehlerursache oben ist still. GitHub benachrichtigt den Autor bei einem fehlgeschlagenen Lauf — aber ein Lauf, der nie startet, erzeugt nichts, worüber benachrichtigt werden könnte.

Das robuste Muster ist ein Dead-Man’s-Switch: Der Workflow pingt bei Erfolg eine Heartbeat-URL, und ein externer Monitor alarmiert, wenn der Ping ausbleibt — egal aus welchem Grund:

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - run: ./run-the-actual-job.sh
      - name: Erfolg melden
        run: curl -fsS https://ping.steadycron.com/${{ secrets.STEADYCRON_PING_TOKEN }}

Jetzt erzeugen ein deaktivierter Workflow, ein verworfener Lauf, ein kaputtes Deployment und die 60-Tage-Deaktivierung dasselbe Ergebnis: einen ausgebliebenen Ping und einen Alarm in Slack oder per E-Mail innerhalb Ihrer Grace-Zeit. Details unter Heartbeat-Monitoring.

Und wenn das Timing selbst zählt (Abrechnungsläufe, Reports): Verlassen Sie sich gar nicht erst auf GitHubs Best-Effort-Scheduler — behalten Sie den Job als workflow_dispatch und triggern Sie ihn per GitHub-API von einem Scheduler mit exaktem Timing und Retries:

# SteadyCron-HTTP-Job → GitHub-API → Ihr Workflow, pünktlich, mit Retries
method: POST
url: https://api.github.com/repos/OWNER/REPO/actions/workflows/nightly.yml/dispatches
headers:
  Authorization: Bearer ${GITHUB_PAT}
  Accept: application/vnd.github+json
body: '{"ref":"main"}'

Das löst drei Probleme auf einmal: exaktes Timing mit Retries, zeitzonenbewusste Zeitpläne, und — weil API-Trigger als Repository-Aktivität zählen — keine 60-Tage-Deaktivierung mehr.

Häufige Fragen

Warum läuft mein geplanter GitHub-Actions-Workflow überhaupt nicht?

Die zwei häufigsten Ursachen: Die Workflow-Datei liegt nicht auf dem Default-Branch (Schedules laufen nur vom Default-Branch), oder GitHub hat den Workflow in einem öffentlichen Repository nach 60 Tagen ohne Aktivität automatisch deaktiviert. Prüfen Sie den Actions-Tab auf ein Deaktivierungs-Banner und ob die Datei auf main existiert.

Welche Zeitzone verwendet GitHub-Actions-Cron?

Immer UTC. Es gibt keine Zeitzonen-Einstellung für on: schedule0 9 * * * bedeutet 09:00 UTC, also 10:00 oder 11:00 in Berlin, je nach Sommerzeit. Die lokale Laufzeit Ihres Jobs verschiebt sich zweimal im Jahr um eine Stunde.

Warum verzögert sich mein GitHub-Actions-Cron um 15–30 Minuten?

Geplante Workflows laufen in einer Best-Effort-Warteschlange. Verzögerungen von 5–30+ Minuten sind normal, am schlimmsten zur vollen Stunde, wenn sich alle 0 * * * *-Jobs stapeln. Eine ungerade Minute (z. B. 17 * * * *) reduziert die Verzögerung — unter hoher Last können Läufe aber komplett entfallen.

Wie oft kann ein GitHub-Actions-Schedule laufen?

Höchstens alle 5 Minuten — */5 * * * * ist die Untergrenze. In der Praxis machen Warteschlangen-Verzögerungen selbst 5-Minuten-Schedules unregelmäßig.

Benachrichtigt mich GitHub, wenn ein geplanter Lauf entfällt oder der Workflow deaktiviert wird?

Für einen verworfenen Lauf gibt es keinerlei Benachrichtigung — ein Lauf, der nie startet, erzeugt nichts, worüber benachrichtigt werden könnte. Bei der 60-Tage-Deaktivierung erscheint ein Banner im Actions-Tab (und eine E-Mail an den Autor), das leicht untergeht. Die zuverlässige Lösung ist ein externer Dead-Man's-Switch, der alarmiert, wenn der erwartete Lauf ausbleibt.