A self-hosted runner fails authentication or token-based API calls because its system clock is wrong. Short-lived/signed tokens validate against time, so skew makes them appear expired or not yet valid.
What this error means
Auth fails intermittently on a self-hosted runner with "Bad credentials" or a JWT "not before"/"expired" error, and the host's clock turns out to be off by minutes. Other runners with correct time work fine.
Actions log
Error: Bad credentials
# or, for app/JWT auth:
'Expiration time' claim ('exp') is in the past / token used before issued
Common causes
Runner system clock drifted
A runner whose clock is wrong (no NTP, VM paused/resumed) signs or validates time-bound tokens incorrectly, so they look expired or not-yet-valid.
GitHub App / OIDC tokens are time-sensitive
App installation JWTs and OIDC tokens carry iat/exp claims validated against the clock. A few minutes of skew is enough to reject them.
How to fix it
Sync the runner clock
Enable time synchronization on the host so token timestamps are valid.
Run an NTP/chrony service on all self-hosted runner hosts.
For VMs, ensure the host resyncs time after pause/resume.
Re-run the failed job after fixing the clock; the auth failure is transient.
How to prevent it
Run NTP/chrony on every self-hosted runner host.
Resync VM clocks after pause/resume.
Treat sudden auth failures with correct credentials as possible clock skew.
Frequently asked questions
What causes "Runner clock skew"?
A runner whose clock is wrong (no NTP, VM paused/resumed) signs or validates time-bound tokens incorrectly, so they look expired or not-yet-valid.
How do I fix Runner clock skew?
Enable time synchronization on the host so token timestamps are valid.
Can Latchkey fix this automatically?
Yes. Latchkey runs your GitHub Actions on managed runners that detect this failure, apply the fix, and retry the job automatically - self-healing is on by default.