Skip to content
Latchkey

GitHub Actions "Bad credentials" / Token Expired From Runner Clock Skew

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.

Terminal
# Linux: enable NTP
sudo timedatectl set-ntp true
timedatectl status   # confirm 'System clock synchronized: yes'

Keep time accurate on the fleet

  1. Run an NTP/chrony service on all self-hosted runner hosts.
  2. For VMs, ensure the host resyncs time after pause/resume.
  3. 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.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →