OAuth Token Refresh for Social Media APIs
TikTok tokens last a day, Facebook gives you sixty, and X with offline access effectively never expires. Covers the three expiry models across 15 platforms, which ones rotate the refresh token and lock you out permanently if you drop it, and the four reasons a valid token still stops working.
OAuth token refresh looks like one problem and is actually three. Tokens expire on wildly different schedules across social media APIs: TikTok gives you 24 hours, Facebook gives you 60 days, X with offline.access effectively gives you forever. Some platforms rotate the refresh token on every use, and losing that new value locks the account out permanently. And a token can stay perfectly valid while the access behind it disappears. This covers all three.
Three expiry models
Every social platform picks one of these, and the choice determines how much machinery you need.
Short-lived access token plus refresh token. The standard OAuth 2.0 arrangement. The access token lives hours, the refresh token lives months, and you trade one for the other. TikTok, Discord, Google (YouTube and Business Profile), Pinterest, Reddit and Bluesky work this way. This model is the most work and the most predictable.
Long-lived token with no refresh token. Meta's approach on Facebook, Instagram and Threads. You exchange a short-lived token for a long-lived one that lasts around 60 days, and you extend it by exchanging it again before it dies. There is no separate refresh credential, which means if you let it expire you cannot recover without the user. No grace period, no admin fix.
Effectively permanent. X with the offline.access scope issues a refresh token that does not expire on a schedule. We store the access token with a sentinel far-future expiry and refresh on demand rather than on a clock. LinkedIn sits between models: the access token lasts 60 days and refresh behaviour depends on the product approval.
The practical consequence: you cannot write one scheduler. A daily job is far too slow for TikTok and absurdly wasteful for X.
Token lifetimes by platform
Verified 02.08.2026. Where a platform does not publish a number, we say so.
| Platform | Access token | Refresh token | Rotates refresh? | Notes |
|---|---|---|---|---|
| ~60 days | none | n/a | Extend by re-exchanging the long-lived token. Page tokens can be non-expiring | |
| Instagram (Facebook Login) | ~60 days | none | n/a | Inherits the Facebook user token lifetime |
| Instagram (Instagram Login) | ~60 days | none | n/a | Refresh endpoint requires the token to be at least 24 hours old |
| Threads | ~60 days | none | n/a | Same model as Instagram Login |
| TikTok | 24 hours | 365 days | Yes | Shortest access token of any platform here |
| X | no fixed expiry with offline.access | long-lived | Yes | We store a sentinel expiry and refresh reactively |
| ~60 days | product-dependent | No | Refresh tokens require an approved product | |
| YouTube / Google | 1 hour | until revoked | No | Refresh token issued only on first consent with access_type=offline |
| Google Business Profile | 1 hour | until revoked | No | Same Google OAuth stack |
| 30 days | 1 year | No | ||
| 1 hour | until revoked | No | Requires a descriptive User-Agent or requests are throttled | |
| Discord | 7 days | until revoked | Yes | |
| Bluesky | short, minutes | session-based | Yes | AT Protocol sessions, not classic OAuth semantics |
| Mastodon | no expiry by default | n/a | n/a | Instance-dependent; some instances do expire |
| Slack | no expiry by default | n/a | n/a | Token rotation is opt-in per app |
| Snapchat | 1 hour | until revoked | No | Standard refresh grant, short access token like Google |
Two rows deserve attention. TikTok at 24 hours means a customer who connects on Friday and does not publish until Monday needs a refresh in between, done by your infrastructure, unprompted. Google at 1 hour is short but forgiving, because the refresh token lives until revoked. TikTok is short and rotating, which is the hardest combination.
Refresh token rotation

This is where integrations lose accounts permanently, and it is worth being blunt about the mechanism.
On a rotating platform, the refresh response contains a new refresh token, and the old one is invalidated the moment the new one is issued. If you do not persist the new value, the account is unrecoverable without user re-authorisation. Not degraded. Gone.
The failure is almost always in the same place: a conditional write.
// Wrong. The response may omit refresh_token on a non-rotating platform, // but on a rotating one this drops the only valid credential you will ever get. if (response.data.refresh_token) { await save({ refreshToken: response.data.refresh_token }); } await save({ accessToken: response.data.access_token });
Two separate writes. If the second succeeds and the first was skipped, or the process dies between them, you now have a fresh access token and a dead refresh token. The account works for the next 24 hours and then stops forever.
// Right. One atomic write, and the refresh token is only overwritten // when the platform actually sent a new one. await saveAtomic({ accessToken: encrypt(response.data.access_token), expiresAt: dateFromNow(response.data.expires_in ?? 3600), ...(response.data.refresh_token ? { refreshToken: encrypt(response.data.refresh_token) } : {}), });
Three rules that follow from this:
Write both tokens in one transaction. Never in two statements, never across two services.
Do not delete the old refresh token until the new one has been used successfully at least once. Some platforms have a short overlap window. Using it is free insurance.
Log the rotation, not the token. When an account dies mysteriously, the question is always "did we get a new refresh token and lose it, or did the platform reject us?" A log line with the account id and a hash prefix answers that in seconds.
Refresh is not the same as validity

A token that has not expired can still be useless. These are four different problems with four different fixes, and treating them as one is why customers get "reconnect your account" for something that reconnecting will not fix.
| Symptom | What actually happened | Fix |
|---|---|---|
Token valid, calls return #463 | Token expired despite your clock saying otherwise | Refresh, or re-authorise if there is no refresh token |
Token valid, calls return #460 | The user changed their password, which invalidates sessions | Only the user can fix this. Prompt re-authorisation |
Token valid, calls return #492 | The user lost their admin role on the Page | Not a token problem. Tell the customer who to re-add, and to which Page |
Token valid, calls return #458 or #459 | Authorisation revoked, or a login checkpoint is required | Re-authorisation, and for checkpoints the user must clear it on the platform first |
The third row is the one that generates the most misdirected support. Your UI says "reconnect Facebook", the user reconnects, it fails again, because the problem is a permission on the Page rather than anything in your database. The message has to name the Page and say "ask an admin to give you a role", or the loop never ends.
The equivalent on the other platforms: LinkedIn returns 403 for a missing org role and for a missing scope with different message text, and Google returns authError for both a revoked grant and a suspended account. Both cases need the message parsed, not just the status code.
bundle.social
Fifteen refresh policies, and one webhook for the moment an account actually dies.
Hosted OAuth, token refresh, and a webhook for the moment an account actually dies.
When to refresh
Two strategies, and the right answer is usually both.
Refresh in the background, on a schedule. A job that scans for tokens expiring within a threshold and refreshes them. Pick a threshold well ahead of expiry: a quarter of the token's lifetime is a reasonable rule, so 6 hours for TikTok and about two weeks for Meta. The advantage is that publish time never pays for a refresh. The disadvantage is that a broken refresh is discovered by your monitoring rather than by an actual failure, which is also the point.
Refresh reactively, before a call that needs it. Check expiry, refresh if close, then publish. This is mandatory as a safety net regardless of what your scheduler does, because a token can be invalidated between scheduler runs.
What breaks if you only do the first: an account connected 30 seconds before a publish has no scheduler run behind it yet. What breaks if you only do the second: a customer who does not publish for a week comes back to a dead TikTok connection and blames you.
Do not refresh on every call. It is wasteful, it burns rate limit, and on rotating platforms it multiplies the number of chances to drop a token.
The race nobody plans for
Two workers refresh the same token at the same time. On a rotating platform, the first call invalidates the refresh token that the second call is holding. The second call fails, and depending on your error handling it may overwrite good state with the result of a failed refresh.
The fix is a lock keyed on the account, not on the worker:
const lockKey = `oauth:refresh:${socialAccountId}`; const acquired = await redis.set(lockKey, workerId, "NX", "EX", 30); if (!acquired) { // Someone else is refreshing. Wait for their result rather than // starting a second refresh that will invalidate theirs. await waitForUnlock(lockKey, { timeoutMs: 30_000 }); return loadAccount(socialAccountId); } try { return await refreshAndPersist(socialAccountId); } finally { await redis.del(lockKey); }
The waiting branch matters as much as the locking branch. A worker that gives up and proceeds with a stale token produces the same failure the lock was meant to prevent.
This becomes non-optional at fleet scale. With a few hundred accounts you might never see it. With tens of thousands of connected accounts and a refresh job running continuously, a collision that happens one time in ten thousand happens several times a day.
The shorter path
Three expiry models, four distinct ways to lose access that all look identical from the outside, rotation semantics that differ per platform, and a locking problem that only appears under load. None of it is conceptually hard. All of it is load-bearing, and it fails quietly.
bundle.social's OAuth API handles the connection flow, the refresh schedule, the rotation writes and the locking for every platform we support. You get a webhook when an account genuinely needs the user's attention, with the reason attached, so your UI can say "ask an admin to re-add you to this Page" instead of "reconnect".
Frequently asked questions
How long do social media API access tokens last?
From 24 hours on TikTok to effectively forever on X with offline.access. Meta platforms sit around 60 days, Google-based platforms at 1 hour with a long-lived refresh token. There is no common default, so token lifetime has to be stored per account rather than assumed.
What happens if I lose the refresh token?
On rotating platforms, the account is unrecoverable without the user authorising again. On non-rotating platforms you can usually keep using the one you have. This is why the refresh token write must be atomic with the access token write.
Which social platforms rotate the refresh token?
TikTok, X, Discord and Bluesky, in our experience. Google, Pinterest and Reddit do not rotate under normal conditions. Meta platforms have no separate refresh token at all - the long-lived exchange they use instead is in Meta's access token guide.
Why does my token stop working before it expires?
Four common reasons: the user changed their password, they revoked the app, they lost their admin role on the Page or organisation, or the platform flagged the account and requires a checkpoint. Only the first two are fixed by reconnecting.
Should I refresh tokens on a schedule or before each call?
Both. A background job at roughly a quarter of the token's lifetime, plus a reactive check before publishing as a safety net. Refreshing on every call wastes rate limit and, on rotating platforms, increases the chance of dropping a token.