401 Unauthorized on /api/v2/analytics/events after token refresh — clock skew?

Running an OpenTelemetry collector in Node.js that aggregates Genesys Cloud Data Action spans. The collector fetches an OAuth token, caches it, and refreshes it 60 seconds before expiry.

The flow works for about 20 minutes. Then, suddenly, every request to /api/v2/analytics/events/query returns a 401 Unauthorized.

Here’s the refresh logic:

async function refreshToken() {
 const response = await axios.post('https://api.mypurecloud.com/oauth/token', {
 grant_type: 'refresh_token',
 refresh_token: storedRefreshToken,
 client_id: clientId,
 client_secret: clientSecret
 });
 
 const newToken = response.data.access_token;
 const expiresAt = Date.now() + (response.data.expires_in * 1000);
 
 // Store new token and expiry
 tokenStore.set('access_token', newToken);
 tokenStore.set('expires_at', expiresAt);
}

The error response body is:

{
 "message": "Unauthorized",
 "errors": ["The access token is invalid or expired."]
}

I checked the logs. The server time on the collector (Asia/Manila) is synced via NTP. The Genesys Cloud servers are likely in the US. Is there a known tolerance for clock skew in the OAuth validation? I tried adding a 30-second buffer to the refresh window, but it still fails.

Also, the expires_in claim in the JWT seems to match the server time, not the client time. If the client clock is slightly behind, the token might still be valid server-side, but my code thinks it’s expired and tries to refresh, which fails if the old token isn’t fully dead yet. Or vice versa.

Any way to force a token refresh without hitting the 401? Or should I just catch the 401 and retry with a fresh token? Feels like a race condition.