Guides

How to Handle OAuth Token Refresh and Expiry for SaaS Integrations at Scale

Every provider handles OAuth refresh differently. Manage token expiry, refresh token rotation, secure storage, and provider policy changes at scale.

Garrett Scott
,
Head of Marketing

How to Handle OAuth Token Refresh and Expiry for SaaS Integrations at Scale

Handling OAuth token refresh at scale means validating token state before every provider call, refreshing with a safety margin ahead of expiry, and rotating tokens atomically so concurrent requests don’t race. The hard part isn’t the refresh call — it’s that every provider implements expiry, rotation, and revocation differently.

The first OAuth integration you build can feel deceptively simple: redirect the user, exchange the code, store the tokens, make the API call.

Where it gets challenging is when that same flow has to work across dozens of providers, thousands of tenants, background syncs, webhooks, retries, and long-lived customer accounts. This is when OAuth stops being a setup step and becomes a reliability issue. Every provider has its own refresh behavior. Every tenant has its own token state. One expired or mishandled refresh token can break a sync, drop records from a workflow, or break an integration the customer relies on every day.

That’s the real problem with OAuth token refresh at scale: not getting a token once, but keeping every customer’s token valid, secure, and recoverable for as long as the integration exists.

OAuth is a standard, and every provider implements it differently

OAuth 2.0 is a framework that leaves refresh behavior deliberately open, and every provider implements it a little differently. RFC 6749 §6 defines the refresh token grant itself in a handful of sentences: the client presents its refresh token to the authorization server’s token endpoint and gets back a new access token, and at the server’s discretion, a new refresh token. Everything past that minimal definition, lifetime, rotation, revocation-on-reuse, per-account caps, is left to the provider. That’s why the table below has six rows instead of one.

Some providers support long-lived refresh tokens, some tie refresh behavior to app-level policy, and others require rotation or additional security controls. As a result, you end up maintaining a slightly different refresh flow for each one, sometimes even across products from the same vendor. Most of the refresh behaviors you’ll encounter follow one of the patterns below.

Two of those patterns are worth naming precisely before the table, because the difference determines when you have to act. A sliding token expiry resets its own clock every time the token is used, so an active connection effectively never expires and only a genuinely idle one does. An absolute token expiry counts down from a fixed point regardless of use, so even a connection you’re actively refreshing will eventually hit its deadline and force re-authorization. The two require different monitoring: a sliding expiry needs you to track last-use per credential; an absolute one needs you to track issue date and warn well before the fixed deadline, since no amount of activity buys more time.

Refresh Behavior

What it Means

What You Have to Handle

Provider Example

Access token, short TTL

Access tokens expire in minutes or hours

Refresh proactively on a margin, or reactively on a 401, before the call fails

Microsoft Entra’s default access tokens run roughly 60–90 minutes, extendable to up to 28 hours under Continuous Access Evaluation

Refresh token valid until revoked

A long-lived refresh token mints new access tokens indefinitely; there’s no expiry clock at all, only revocation

Long-term secure storage; a rotation path if a token is compromised

Google’s classic offline-access flow reuses the same refresh token until it’s revoked, expired, or capped; Salesforce’s default connected-app policy is also “valid until revoked”

Refresh token expires after inactivity (sliding)

The refresh token dies if unused within a window; using it resets the clock

Track last use per credential; refresh before the inactivity window closes so the grant doesn’t lapse

Microsoft Entra’s 90-day refresh-token max-inactive-time

Refresh token expires on a fixed schedule (absolute)

The grant dies at a set point regardless of use

Track issue date; prompt re-authorization proactively, since activity won’t extend the deadline

Salesforce’s optional “expire refresh token after N days” policy; Google’s 7-day refresh-token expiry for apps still in “Testing” consent-screen status

Refresh token capped per account or client

Providers can cap how many active refresh tokens exist at once; exceeding the cap silently evicts the oldest

Don’t assume unlimited concurrent grants; monitor for silent invalidation, not just explicit revocation

Google caps refresh tokens at 100 per account per OAuth client ID

Refresh token rotates on every use

Each refresh returns a new refresh token; the previous one is invalidated immediately

Persist the newest token atomically; prevent concurrent-refresh races; treat reuse of an invalidated token as a compromise signal

Slack’s mandatory Token Rotation, which can’t be turned off once enabled; Salesforce’s opt-in Refresh Token Rotation, moving toward mandatory

None of these are edge cases. They’re the default behavior of major providers you’re already connecting to, and the policy can change under you without warning. “Handle token refresh” isn’t one engineering task. It’s six, multiplied by every provider in your catalog.

Provider policies you can’t assume away

(Current as of August 2026 — the facts in this section are provider policy, not spec, and providers change policy on their own schedule. Verify against each provider’s current documentation before treating a specific date or cap as fixed.)

Three providers illustrate why “handle OAuth refresh” keeps expanding into provider-specific work.

Salesforce is rolling out mandatory Refresh Token Rotation: once enabled, each refresh issues a single-use refresh token and immediately invalidates the one you just used, and reusing an already-rotated token revokes the current refresh token and every access token tied to it, forcing a full re-authorization. Salesforce names the Summer ‘26 release wave for enforcement, and its ISVforce guidance puts rotation alongside PKCE among the controls ISV-distributed connected apps must meet — verify both scopes against current Release Notes before treating either date as settled.

Google does three things worth knowing before you build against it: apps whose consent screen is still in “Testing” status get a refresh token that expires in seven days regardless of scopes (move to “In production” to fix it); refresh tokens are capped at 100 per account per OAuth client ID, and exceeding the cap silently invalidates the oldest one with no error to catch; and in the classic offline-access flow, Google does not rotate the refresh token on every use, the same token is reused until it’s revoked, expired, or capped, the opposite assumption you’d carry over from a rotation-first provider like Salesforce or Slack.

Microsoft Entra ID has used fixed, non-configurable token lifetimes since January 2021: a 90-day max-inactive-time for refresh tokens and an until-revoked maximum age. In their place, Entra runs Continuous Access Evaluation, a near-real-time channel that can challenge or revoke a token on events like a disabled account or a password change instead of waiting for TTL expiry, with up to 15 minutes of event-propagation latency and access tokens extending to as long as 28 hours as a result.

A refresh implementation that assumes one provider’s behavior, indefinite reuse, fixed rotation, or a configurable lifetime, will eventually meet a provider that does the opposite, usually in production.

Validate token state before each provider call

The core loop in the token refresh lifecycle is simple to describe but easy to get wrong. Before each third-party API call, check whether the access token is close to expiring. Use a proactive refresh (a TTL margin check): refresh before the exact expiry time, leaving a buffer for clock drift, latency, retries, or long-running jobs.

That proactive path handles predictable expiry before the provider rejects the request, but it won’t catch everything. A provider can revoke a token early, shorten a session, or reject a token because of clock drift or user action. That is why you also need the reactive path: if the API call returns 401, refresh once, store the new token, and retry the request.

That’s two separate paths through the same operations: one triggered by the expiry check, one triggered by a failed call. Both need to land in the same place: a stored token, then a successful response. In pseudocode, the loop looks like this:

# Before every third-party API call if token.expires_within(safety_margin): token = refresh(token) # idempotent; handles rotation store_atomic(tenant, provider, token) response = call(api, token) if response.status == 401: # token died early token = refresh(token) store_atomic(tenant, provider, token) response = call(api, token)

Both paths should end the same way: the newest token is stored, and the API call succeeds. If the provider rotates refresh tokens, that storage step has to be atomic. Otherwise, concurrent refreshes can overwrite each other and leave your system with a refresh token the provider has already invalidated. That concurrent-refresh race is the failure mode to solve next.

Concurrency is the real risk in refresh-token rotation

Refresh token rotation is safe when one worker refreshes one connection at a time. When a provider rotates refresh tokens on every use, each successful refresh invalidates the token you just used and returns a new one. That is a good security property, but under concurrency, it can leave your system storing a refresh token the provider no longer accepts. If two workers refresh the same connection simultaneously, one can overwrite the other and leave your system storing a refresh token the provider no longer accepts.

This bug usually doesn’t show up in staging. It shows up at scale, under real concurrency, when cron syncs, retry logic, and a user clicking “sync now” all fire against the same tenant. The fix is a single-flight refresh per tenant-provider pair: only one refresh runs for that connection, and the new token is persisted atomically before another worker can use stale credentials. If refresh-token rotation is possible, single-flight refresh and atomic persistence should be treated as correctness requirements, not optimizations.

This is not a niche precaution. Auth0 and Okta both document refresh-token rotation as a recommended pattern for reducing refresh-token risk, and RFC 9700, published as the OAuth 2.0 Security Best Current Practice (BCP 240), requires it, sender-constraining or rotation, as a MUST for public clients specifically, not a blanket rule for every client type. Slack and Salesforce are both moving the same direction regardless of client type: Slack’s Token Rotation is opt-in, but irreversible once you enable it, and Salesforce’s Refresh Token Rotation is headed the same way. If you have not built for rotation yet, plan to: more providers are likely to require it over time.

Refresh storms: when many tokens expire or refresh at once

A refresh storm is what happens when a large batch of tokens hits the provider's token endpoint at once, either because they were all issued together (a bulk migration, a mass re-auth campaign) and now expire together, or because many concurrent requests on one connection see the same expired token simultaneously and each fires its own refresh. Either way, you get a correlated burst instead of a smooth trickle, right when the provider is least likely to tolerate it.

The fix borrows from general resilience engineering, not anything OAuth-specific. AWS's own guidance on exponential backoff found that backoff alone doesn't break the correlation, clients on the same fixed schedule simply re-collide on the next attempt, the fix is jitter: randomizing each client's schedule so retries spread out instead of re-synchronizing. Google's SRE Book applies the same logic to retry budgets: cap retries at a fraction of total request volume, cap attempts per request, and propagate the failure instead of retrying indefinitely once the budget's spent. A worker pool refreshing many tenants against one provider needs that same jittered backoff and retry ceiling any high-volume client of that provider would need, not a bespoke OAuth mechanism.

Retrying blindly also assumes the failure is temporary, and it usually isn't. The OAuth spec's invalid_grant error signals a refresh token that's permanently dead, revoked, expired, or reused after rotation, not a transient network blip or a concurrency-induced 401. Routing invalid_grant straight to re-authorization instead of retrying it in the same backoff loop keeps a dead connection from consuming retry budget that live connections need. For why authenticating to a customer's own connections is inherently harder than internal, system-to-system auth in the first place, see why authenticating to customer-facing integrations is hard.

One honest gap: there's no independent, named-company postmortem of an OAuth-refresh-storm incident specifically, the backoff and retry-budget material above is general, well-established practice applied here, not an OAuth-native source. The same honesty applies to customer notification, batching notifications so a customer isn't spammed with one email per broken connection is common-sense practitioner judgment, not something backed by a published source we could find.

Token storage is where security breaches actually happen

A stored OAuth token is a live credential into a customer's business system:

  • Their CRM

  • Their support desk

  • Their HR data

And that's how you should be treating it: encrypt the token even when it's at rest, run a real secrets management layer, lock down access so a compromised service can't read every tenant's tokens, and pen-test the storage layer regularly. Build a real revocation path too, so a compromised token can be invalidated instead of waiting out its TTL.

This is not a place to vibe-code a solution and revisit it later. A compromised token doesn't just break your integration. It hands an attacker a working session into a customer's business-critical data, under your name.

A token store breach doesn't stay contained to one integration. It exposes every tenant whose tokens live in that store, so the risk scales directly with your customer count, which is exactly why the isolation model matters as much as the refresh loop. For the architecture that keeps one tenant's compromise from reaching another's, see tenant isolation architecture for OAuth credentials, and for the encryption schemes and key-rotation mechanics behind that storage layer specifically, see multi-tenant OAuth token storage architecture.

You don't get a vote when providers change OAuth

OAuth policies are not static, and providers change them on their own schedule. Here are some recent examples, as of this publication, that illustrate how often operational contracts can move:

  • Zendesk announced default expiration for OAuth access and refresh tokens for global clients beginning February 2, 2026, with local clients scheduled to adopt refresh-token flows by April 1, 2027.

  • BambooHR deprecated its legacy OpenID Connect login flow for new integrations on April 14, 2025, moving new marketplace apps to OAuth 2.0.

  • Salesforce announced a PKCE requirement for connected apps and external client apps, with a May 11, 2026 deadline for affected apps, following a period of heightened concern around malicious connected-app authorization. Salesforce is also rolling out the mandatory Refresh Token Rotation described above, named for the Summer '26 release wave; its ISVforce guidance groups rotation with PKCE among the controls ISV-distributed connected apps must satisfy, so confirm which deadline applies to your app type.

  • Linear migrated OAuth2 applications to a new refresh-token system on April 1, 2026, changing how apps handle access-token expiry and refresh.

None of these were breaking changes to the OAuth spec. They were policy changes inside providers' own implementations, the kind you only catch by watching changelogs across every provider in your catalog, on their schedule, not yours. Miss one, and that provider's integrations start failing in customer accounts with no code change on your side to point to.

OAuth maintenance multiplies across integrations and tenants

Every pattern we've covered here has to work per tenant, per provider, every time: proactive and reactive refresh, rotation handling, provider-specific expiry rules, refresh-storm resilience, secure storage, and provider-change monitoring. Even a modest provider catalog becomes a matrix of token state: each customer connection has its own expiry behavior, re-authorization path, failure signature, and vendor changelog to track.

At that point, building it yourself means owning the operating model around it. It's your team that's responsible for provider monitoring, token failure alerts, re-auth recovery paths, incident response, and concurrency bugs that only appear under production load. Someone owns the on-call burden. Someone reads every provider's changelog. And someone gets paged when a rotation race takes down a tenant's connection at 2 a.m.

Offloading token management centralizes refresh, expiry, and storage

The alternative is to stop owning the per-provider refresh contract entirely. Paragon Auth is a managed authentication layer that handles refresh and expiry across supported SaaS integrations and custom integrations, behind a single integration point. This means proactive refresh, reactive fallback, rotation handling, provider-specific expiry rules, and secure storage are solved once, not once for every provider.

The product ships with an embeddable auth experience, Connect Portal, so your users authorize integrations without you building and maintaining that UI yourself. It also supports on-premise deployment for teams with stricter security requirements around where tokens live.

The point is not to remove OAuth from your architecture. It is to stop rebuilding provider-specific refresh, rotation, storage, re-authorization, and policy-change handling for every integration.

For more on the build-versus-buy tradeoff behind this decision, see our guide to multi-tenant auth for customer-facing integrations and keeping integrations working when third-party APIs change.

FAQ

What's the right way to handle OAuth token refresh and expiry at scale? At scale, OAuth refresh gets hard because every SaaS provider-tenant connection has its own token lifecycle that's outside of your control. You can handle this by offloading the per-provider refresh contract to a managed auth layer like Paragon Auth. Alternatively, you can manage it yourself by refreshing before expiry, retrying once on 401, storing rotated tokens atomically, securing tokens at rest, monitoring provider changes, and prompting re-authorization when refresh fails.

What happens if an OAuth token expires and isn't refreshed? You'll usually see it in the customer's account before you see it in your monitoring. The integration just errors out, often without throwing anything you'd catch, unless you're actively checking for it.

How is OAuth token handling different across SaaS providers? Because the OAuth spec leaves expiry and rotation open, providers use that freedom differently: Google reuses one refresh token until it's revoked or capped, Salesforce and Slack are both moving toward mandatory rotation, and Microsoft Entra replaced configurable lifetimes with event-driven revocation. Paragon Auth normalizes these differences behind one integration point, so your app doesn't maintain a separate refresh flow per provider.

What is refresh token rotation, and which providers require it? Every time you refresh, you get a new refresh token and the old one stops working immediately, reusing the old one is treated as a compromise signal, not a retryable error. Slack makes it opt-in but permanent once enabled, and Salesforce is moving it toward mandatory. Store the newest token atomically, or a second request refreshing at the same time will quietly break the connection.

How do you store OAuth tokens securely at scale? Treat every stored token as a live credential: keep it encrypted, keep the encryption keys apart from the values they protect, restrict who and what can read them, and test the storage layer against real attacks on a schedule, not just at launch. Paragon Auth handles that encryption and access control as part of its managed layer, so it's solved once instead of per integration.

What's the difference between sliding and absolute token expiry? A sliding expiry resets every time the credential is used, so an active connection stays alive and only a genuinely idle one lapses, Microsoft Entra's 90-day inactivity window works this way. An absolute expiry counts down from a fixed point no matter how active the connection is, the way Salesforce's optional fixed-day refresh-token policy does, so activity alone won't save it.

Should you build OAuth refresh yourself, or hand it off? Hand it off once you're past a couple of providers. Paragon Auth handles refresh and expiry across its integration catalog and for custom integrations, so your team isn't building and maintaining a per-provider refresh contract, and re-verifying it against every provider's next policy change, in-house.

Related

TABLE OF CONTENTS
    Table of contents will appear here.
Ship native integrations 7x faster with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon