OAuth Token Desync caused by 502 Origin Gateway Errors on Refresh Endpoint

We are building an integration using the WHOOP API and are running into a frustrating “Token Desync” issue that is forcing our users to repeatedly re-authenticate. I’m curious if any other developers here have run into this or found a workaround.

The Scenario: When our back-end attempts to refresh an access token via https://api.prod.whoop.com/oauth/oauth2/token, we occasionally receive a 502 Bad Gateway from Cloudflare (error_name: origin_bad_gateway). The Cloudflare payload specifically states that the origin web server returned an invalid or incomplete response.

Because we receive a 502, our server never receives the new refresh_token. However, it appears WHOOP’s backend successfully processed the token rotation before the gateway timeout occurred.

When our cron job attempts to retry the refresh 15 minutes later using the “old” refresh token, WHOOP rejects it with a 400 Bad Request (invalid_grant). The token has been permanently desynced, and the user is disconnected.

My Questions:

Has anyone else experienced these intermittent 502 gateway timeouts when refreshing tokens?
Does WHOOP offer any sort of Refresh Token Grace Period? Many OAuth providers (like Auth0 or Okta) allow the old refresh token to remain valid for a short window (e.g., 60 seconds) specifically to prevent desyncs caused by network failures like this.
If any WHOOP engineers browse these forums, here is a recent request where your origin dropped the response during rotation:

Timestamp: 2026-08-03T04:00:17Z
Cloudflare Ray ID: a25269c76d181f5e
Zone: api.prod.whoop.com
Any insights or workarounds from the community would be hugely appreciated!

Ran into the exact same “permanent desync on the old refresh token” symptom, but for us the trigger was usually not a 502 — it was a client-side read timeout.

In our logs, actual 502 Bad Gateways from the token endpoint were rare (a couple back in May). The recurring failure was our HTTP client raising a ReadTimeout on the POST /oauth/oauth2/token refresh. The mechanism is identical to what you describe: WHOOP rotates and invalidates the old refresh token server-side, but if our client gives up before receiving the response, we never persist the successor → next refresh with the “old” token returns 400 invalid_grant → user disconnected.

The root cause was latency, not errors. Our HTTP client defaulted to a 5-second timeout, and WHOOP’s token endpoint has been answering cold requests (token idle for a few hours, first refresh of the session) well above that. We’ve measured successful refresh calls taking 7–11 seconds. So a 5 s default was aborting the exchange right in the rotation window.

One thing fixed it for us: Raise the token-refresh HTTP timeout well above WHOOP’s p99 (we went from 5 s → 12 s). That stopped the desyncs.

Seconding your grace-period request, though. The deeper issue is that rotation has no overlap window, so any lost response (502 or a client timeout or a dropped connection) is unrecoverable. A short window where the previous refresh token stays valid (like Auth0/Okta) would make all of these network-level failures survivable instead of fatal. Until then, we are giving a generous timeout + treating any non-2xx/timeout on refresh as “assume rotated” and alerting.

Thanks for sharing this—it’s really helpful. Our case is slightly different in that our HTTP client isn’t timing out; we’re actually receiving a Cloudflare 502 origin_bad_gateway response from api.prod.whoop.com. But the end result is exactly the same: it appears the refresh token has already been rotated server-side, while our application never receives the new token, leading to a permanent invalid_grant on the next refresh.

Hopefully the WHOOP team can comment on whether a short overlap window for refresh tokens, or some other recovery mechanism, is being considered. It would make these unavoidable network failures much more resilient.

We’re seeing the exact same failure mode, and it’s recurring. Across two separate apps (different client IDs), we’ve had at least six occurrences since late July — the latest being three separate windows yesterday (Aug 5): around 15:00, 18:00 and 19:00 UTC.

The fingerprint is always identical: POST /oauth/oauth2/token hangs for 10–18s (normally <2s for us), then returns a Cloudflare 502 (origin_bad_gateway) — the rotation was already processed at the origin but the response was lost. Every later attempt with the stored refresh token gets a 400 (in our case the generic invalid_request boilerplate rather than invalid_grant), and the user has to fully re-authorize. In the Aug 5 19:00 UTC window, two independent connections hit the 502 in the same second while refreshes seconds before and after succeeded — short bursts of origin instability rather than a full outage. status.whoop.com showed nothing. We already never retry the token call (rotation isn’t idempotent), so there’s no client-side mitigation left — +1 to the Auth0/Okta-style grace period suggestion.

Questions for the WHOOP team:

  1. Could this be related to apps still pending production approval? We’re waiting on approval to raise our user and request limits — does a pending review affect token-endpoint reliability in any way, or are dev-mode and production apps served identically?
  2. What timeline can we expect for a fix on the origin 502s (or for a refresh-token grace period)? This directly disconnects end users, so even a rough ETA would help.

OK I’ve had some 502s recently and spent a day digging into this:

The issue: WHOOP’s token endpoint slows down on the hour (12:00, 1:00, 2.00 etc.). Refreshes at those times that get slow enough cross an origin timeout and come back 502… after the rotation has already committed, which is what desyncs the grant.

Why it likely happens: everyone’s 0 * * * * fires at once. Requests queue in front of the origin, and the tokens last exactly 1 hour!

The fix: move your refresh off :00/:15/:30/:45 to some arbitrary offset, and refresh ahead of time rather than at the moment a job needs the token — otherwise an expiry during an on-the-hour job drags you straight back into the bad window.

@skumar0307 your Ray ID timestamp is 04:00:17Z.
@pauloernani yours were 15:00, 18:00 and 19:00 UTC, and the 10–18s hang you describe matches.

WHOOP staff. this is a problem that affects all API users, please escalate internally.

Full write up with data, charts and recommendations: Claude Artifact

Adding a data point since this thread hasn’t had a WHOOP response yet and the pattern in my own setup matches exactly

what’s described here.

I run a small script that refreshes my OAuth token daily via the standard refresh_token grant. It’s been dying with a

hard 400 invalid_request on refresh — not intermittently retryable, genuinely dead, requiring a full interactive

re-auth (log in again in a browser) to recover, since there’s no device/headless flow available.

Recurrence pattern (2026): Jun 28, then accelerating — Jul 21, Jul 23, Jul 25, Jul 30, Aug 2, Aug 4, Aug 5, Aug 6,

Aug 7, Aug 9. That’s roughly daily-to-every-other-day by early August, down from ~2-day gaps in late July.

Confirmed it’s a genuinely dead token, not a transient network blip: before touching anything, I ran the exact

refresh call by hand with the script’s real client_id/params and got a clean 400 invalid_request straight from the

token endpoint — not a Cloudflare error page, an actual “this token doesn’t exist” response from your auth server.

That’s consistent with the theory in the OP: the refresh token gets rotated server-side even when the client-facing

response is a 502, so the client is left holding a token that’s already been invalidated.

No client-side mitigation is possible — I’ve only been able to reduce time-to-detection (fail fast on the first 400

instead of blind-retrying and losing a day), not prevent it. Every recovery requires a real interactive login;

there’s no way to resync automatically once the desync happens.

Given the accelerating frequency, would appreciate an acknowledgment from WHOOP eng — this is making the

refresh_token grant close to unusable for any unattended/scheduled integration.

Adding data, since I’ve been fighting this for eleven days and I log every refresh attempt.

Setup is a single-user personal sync, Python 3.14 and httpx 0.28.1, against https://api.prod.whoop.com/oauth/oauth2/token.

Since July 30 I’ve taken 25 502s on that endpoint. Eight of them killed the grant outright and forced me to go re-authorize in a browser. The ones that opened an outage:

2026-07-31 20:00:03 UTC   502 after 11.2s

2026-08-01 19:00:01 UTC 502 after 11.8s

2026-08-03 12:00:14 UTC 502 after 15.7s

2026-08-04 20:00:02 UTC 502 after 12.5s

2026-08-07 12:00:03 UTC 502 after 16.4s

2026-08-08 12:00:01 UTC 502 after 11.5s

2026-08-08 14:00:04 UTC 502 after 15.1s

2026-08-09 12:00:08 UTC 502 after 12.1s

On the timeout suggestion, I don’t think it helps, at least not here. All 25 came back as real HTTP responses. No connection errors, no timeouts, and my client already sits at 30s. Fastest was 5.7s, median 11.3, slowest 17.9. The origin is answering, just slowly and with a 502, so raising the timeout wouldn’t have caught a single one of mine.

The thing that cost me two days: my follow-up error is always invalid_request. Thirty-six of them, never once invalid_grant. On Hydra/fosite that’s the non-expiry validation branch, and its default hint helpfully mentions redirect_uri, which has nothing to do with a refresh grant. I went hunting through my own client twice before working out that the 502 in front of it was the whole story. Since skumar0307 is getting invalid_grant and I’m getting invalid_request, I’d stop trusting the code entirely and just look for a 502 before it.

What I can’t do is retry. Replaying a refresh token after a 502 is how you get the whole chain revoked for reuse, so once this happens there’s nothing left on my end except asking a human to click through consent again. That’s the actual problem. Losing a sync is nothing. Losing it in a way only a person can undo, every day or two, makes an unattended integration pointless.

Before anyone asks whether I’m hammering the endpoint: I’m not. I hold an access token for its full hour, so 249 pulls over these eleven days produced only 134 refreshes, about one an hour. I’ve since cut that to five a day by spacing my pulls further apart, which should reduce how often I’m exposed but obviously fixes nothing. And pauloernani’s Aug 5 failures were at 15:00, 18:00 and 19:00 UTC while mine that day were at 12:00 and 12:30, so whatever this is, it isn’t following any one account’s request pattern.

One loose thread, take it or leave it: four of my eight outages opened on the first refresh after sitting idle overnight, roughly 13 hours. Idle-overnight refreshes failed 4 of 24 for me, against 7 of 110 for the hourly ones. Small numbers, could easily be noise.

What would actually fix this is a short grace period on the previous refresh token, even ten or twenty seconds, so a rotation my client never received isn’t fatal. Failing that, only commit the rotation once the response has actually gone out. Either one turns this from a dead grant into a retry.

Is anyone from WHOOP reading this thread? It’s been six days. Happy to hand over raw logs, request and response pairs, or token fingerprints for any of the timestamps above.

I feel your pain @zefross

The temporary fix is relatively simple though.

Just change your script to update the token at any time other than on the hour (1:00, 2:00. 3:00, etc.)
So make it update at 1:27, 2:27, 3:27, etc. or some other part of the hour. There are only 4 minutes you need to avoid. the rest work fast and efficiently with no 502s.

But yes, WHOOP need to fix their capacity or change their refresh policy. ¯\_(ツ)_/¯

Best of luck!