Expired token errors on GPT-3.5 after GPU node autoscaling

Problem Statement

During peak load on a GPU‑accelerated inference cluster (mixed A100/H100 nodes), API calls to OpenAI GPT‑3.5 intermittently fail with 401 Unauthorized errors. The failure manifests as “expired token” messages in application logs, causing request timeouts and downstream job failures.

Typical log excerpt from a worker pod:

2026-10-21T14:32:07.842Z ERROR request_id=7f3c2a1b TokenRefreshError: Token expired at 2026-10-21T14:31:58Z
2026-10-21T14:32:07.845Z INFO  request_id=7f3c2a1b Retrying request after token refresh...
2026-10-21T14:32:08.112Z ERROR request_id=7f3c2a1b OpenAIAPIError: 401 Unauthorized - Invalid authentication credentials

The issue appears only after the autoscaler adds new GPU nodes; existing nodes continue to operate normally.

Root Cause Analysis

Token lifecycle in the client library

The internal OpenAIClient caches an OAuth‑style bearer token with a expires_at timestamp (usually 1 hour). The token is refreshed by invoking the /v1/auth/token endpoint. The cache is stored in a process‑local singleton.

Interaction with autoscaling

When the Kubernetes autoscaler provisions a new node, the pod scheduler launches fresh worker containers. These containers inherit the same OPENAI_API_KEY secret but start with an empty in‑memory token cache. The first request triggers a token fetch, which succeeds. However, the node’s system clock is not yet synchronized with NTP, resulting in a clock skew of +5 minutes.

Because the token’s expires_at field is interpreted using the local clock, the client believes the token is already expired and immediately attempts a refresh. The refresh request includes the stale expires_at in the request body (some client versions send it for diagnostic purposes), causing the OpenAI server to reject the request with invalid_grant. The client then falls back to the original token, which it still treats as expired, leading to the observed “expired token” error.

Why only after autoscaling?

  • Existing pods have already synchronized clocks via chrony at startup.
  • New nodes join the cluster before the chrony daemon has completed its initial sync, leaving them with a skewed system time.
  • The token cache is per‑process; there is no shared state to propagate a valid token from a healthy node.

Investigation and Debugging

Step 1 – Correlate errors with node lifecycle

# List recent node additions
kubectl get nodes --sort-by=.metadata.creationTimestamp -o wide | tail -n 5

# Example output
NAME            STATUS   ROLES    AGE   VERSION   INTERNAL-IP   OS-IMAGE             KERNEL-VERSION
gpu-node-12     Ready       3m    v1.27.3   10.2.1.12    Ubuntu 22.04.3 LTS   5.15.0-105-generic
gpu-node-13     Ready       2m    v1.27.3   10.2.1.13    Ubuntu 22.04.3 LTS   5.15.0-105-generic

Cross‑reference the timestamps of the TokenRefreshError logs with the node creation times; they line up.

Step 2 – Verify clock skew on affected pods

# Exec into a pod on a newly created node
kubectl exec -it inference-7f9c9d9c9-abcde --date

# Sample output
Fri Oct 21 14:27:58 UTC 2026   # 5 minutes ahead of NTP server

Step 3 – Inspect token cache contents

# Inside the Python worker process
import json, os
print(open("/tmp/openai_token_cache.json").read())

# Sample cache (expires_at is in UTC)
{
  "access_token": "sk-...",
  "expires_at": "2026-10-21T14:31:58Z"
}

The expires_at value is correct, but the local clock interprets it as already passed.

Step 4 – Reproduce the failure locally

# Simulate clock skew
sudo date -s "2026-10-21 14:35:00 UTC"

# Run a minimal request
python - <<'PY'
import openai, os
openai.api_key = os.getenv("OPENAI_API_KEY")
try:
    resp = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role":"user","content":"Hello"}]
    )
    print(resp)
except Exception as e:
    print("Error:", e)
PY
# Expected error
Error: TokenRefreshError: Token expired at 2026-10-21T14:31:58Z

Resolution

Fix 1 – Enforce NTP sync before starting inference containers

Add an init container that blocks until chronyc waitsync 30 succeeds.

apiVersion: v1
kind: Pod
metadata:
  name: inference-worker
spec:
  initContainers:
  - name: ntp-wait
    image: alpine:3.18
    command: ["sh", "-c", "apk add --no-cache chrony && chronyc waitsync 30"]
    securityContext:
      privileged: true
  containers:
  - name: inference
    image: myrepo/inference:latest
    envFrom:
    - secretRef:
        name: openai-secret
    # ... rest of spec

This guarantees that the system clock is within 1 second of the NTP source before the Python process starts.

Fix 2 – Make token cache tolerant to minor clock skew

Patch the client library to apply a safety margin (e.g., 30 seconds) when evaluating expires_at.

# Before (client library v0.27.0)
if datetime.utcnow() >= token["expires_at"]:
    raise TokenRefreshError("Token expired")

# After patch
SKewMargin = timedelta(seconds=30)

if datetime.utcnow() + SkewMargin >= token["expires_at"]:
    # Refresh proactively
    token = self._refresh_token()

Apply the patch by vendoring the library or by upgrading to openai>=0.28.1, which includes this guard.

Fix 3 – Share a refreshed token via a ConfigMap

Deploy a sidecar that periodically fetches a fresh token and writes it to a shared volume. Workers read the token from the file instead of maintaining a private cache.

# token-sidecar.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: token-sidecar
spec:
  replicas: 1
  selector:
    matchLabels:
      app: token-sidecar
  template:
    metadata:
      labels:
        app: token-sidecar
    spec:
      containers:
      - name: refresher
        image: python:3.11-slim
        command: ["python", "-u", "-c", "
import time, os, openai, json
while True:
    t = openai.AuthToken.create()
    with open('/shared/token.json', 'w') as f:
        json.dump(t, f)
    time.sleep(300)
"]
        volumeMounts:
        - name: token-volume
          mountPath: /shared
      volumes:
      - name: token-volume
        emptyDir: {}

Workers mount the same emptyDir and read /shared/token.json before each request.

Verification

Validate clock synchronization

# On a newly created node
chronyc tracking
# Expected output includes:
Reference ID    : 203.0.113.1 (ntp.example.com)
System time     : 0.000123 seconds fast

All nodes should report a drift < 0.5 seconds.

Confirm token refresh works under load

Successful run should output “No errors”.

Prevention and Best Practices

  • Enforce time sync at pod start. Use an init container or a DaemonSet that ensures chronyd is healthy on every node.
  • Centralize token management. A sidecar or external secret store reduces per‑process cache inconsistencies.
  • Upgrade client libraries regularly. Versions ≥0.28.1 include a built‑in clock‑skew tolerance.
  • Monitor clock drift. Export node_time_offset_seconds from node_exporter and alert if >1 second.
  • Graceful token refresh. Implement exponential backoff on invalid_grant responses to avoid thundering‑herd retries after a node join.

Frequently Asked Questions

  1. Why do only some requests fail after a node is added? The failure is tied to the node’s system clock. Pods on nodes with correct time succeed, while those on nodes still syncing experience token expiry miscalculations.
  2. Can I disable the token cache to avoid this problem? Disabling the cache forces a token fetch on every request, which mitigates expiry misinterpretation but adds latency and rate‑limit pressure. Centralizing the token (sidecar) is a better approach.
  3. Is the issue specific to A100 vs. H100 nodes? No. The hardware type is irrelevant; the root cause is the timing of NTP synchronization during node provisioning.
  4. Do I need to rotate the OpenAI API key after fixing the clock? No. The API key remains valid; only the bearer token’s perceived expiry was incorrect.
  5. How do I detect clock skew before it impacts production? Add a Prometheus rule:
    node_time_offset_seconds{job="node-exporter"} > 1

    and configure an alert to fire on sustained offset.

Related Topic Hub: LLM Systems Troubleshooting Hub