#17582·vercel

@vercel/oidc: deployed Functions have no token-refresh path, so work outliving the 2h TTL fails unrecoverably

Author: shtefcsCreated Sep 4, 2026Updated Sep 9, 2026

Summary

@vercel/oidc's only token-refresh path reads Vercel CLI files from a developer machine. In a deployed Function those files don't exist, so once the OIDC token expires there is no recovery — every subsequent call throws Could not get credentials from OIDC context.

For request-shaped work this never surfaces, because the docs guarantee the token outlives a single invocation. For work that spans many invocations — Workflow runs, long-lived Sandbox sessions, agent batches — it surfaces reliably, and there is no documented way to recover.

We hit this in production: 7,565 failed jobs, all inside a window the docs predict exactly.

Why the guarantee doesn't cover this shape

From /docs/oidc:

Vercel does not generate a fresh OIDC token for each execution. It reuses a token for up to 90 minutes. Function tokens have a Time to Live (TTL) of two hours. The remaining 30 minutes ensures the token stays valid throughout a Function's maximum execution duration.

The margin is sized to one Function's max duration. That's a sound guarantee for a request. But a Workflow run, or a Sandbox session reattached across steps, is not one Function — it's dozens of invocations over hours, sharing one long-lived resource. A token picked up at 89 minutes old leaves 31 minutes, and the work continues well past that.

Measured behaviour

Six independent batches, started days apart, each first failing partway into its own run:

batch first OIDC failure last batch age at time of query
A 46.5 min 89.2 min 2,403 min
B 38.1 min 116.2 min 1,599 min
C 54.0 min 61.1 min 1,433 min
D 56.8 min 63.4 min 1,433 min
E 70.5 min 74.7 min 11,164 min
F 70.3 min 70.3 min 2,254 min

Both bounds match the documented model: first failures land in the 30–120 min band left over after up to 90 minutes of reuse, and nothing fails later than the 2-hour TTL (max observed 116.2 min).

Batch E is the informative one — it is 7.7 days old and completed 4,791 jobs. It kept working long after its failure window, so this is not "the run died"; it is a credential going stale and later being replaced.

Mechanism

getVercelOidcToken()isExpired(...)refreshToken(), and refreshToken resolves project/team from the filesystem (.vercel/project.json) and credentials from com.vercel.cli / com.vercel.token. None exist in a deployed Function, so the refresh throws and @vercel/sandbox surfaces VercelOidcContextError — the branch gated on process.env.VERCEL_URL, i.e. the deployed path specifically.

So the failure isn't "token expired"; it's "token expired and the only renewal path is a dev-machine flow".

The question

What is the recommended way to authenticate work that outlives a single Function invocation?

Concretely, for Workflow runs and Sandbox sessions spanning hours:

  1. Is the intended answer simply "use an access token triple (token / teamId / projectId) for anything long-running", and should the OIDC docs say so? That's what we've now done, and it works — but it reintroduces exactly the long-lived credential OIDC exists to remove, which feels like the wrong end state for first-party Vercel primitives.
  2. Is there a supported way for a deployed Function to mint or exchange a fresh OIDC token, rather than only consuming the injected one? A production-side equivalent of refreshToken() would close this cleanly.
  3. Should long-running SDKs re-read x-vercel-oidc-token per operation rather than resolving once? That helps a per-request path but not a resource created once and reused for hours.

Guidance on the intended pattern would be more valuable to us than a code change — we'd rather build it the way you intend.

Related

All three existing OIDC-refresh issues concern the dev path; none covers the production case:

  • #14685 — favour existing token claims over filesystem settings when refreshing
  • #15256 — delegate dev refresh to the vercel project token CLI. Worth flagging: this removes the OAuth refresh entirely, which is right for dev, but production has no path either way — so afterwards the only refresh mechanism would be CLI-shaped. cc @gr2m as author.
  • #14548 — CLI daemon for managing auth and OIDC tokens

Same error reached from adjacent directions:

  • vercel/ai#16695 / vercel/ai#16500 — @ai-sdk/sandbox-vercel resumeSession() reattaches via Sandbox.get({ name }) without forwarding credentials, falling back to OIDC. Same failure, different cause (dropped credentials rather than expiry).
  • vercel/eve#960 — the sandbox creation path never consults its own env-credential resolver, so an explicitly-configured triple is ignored and it falls back to OIDC-only. Notably that issue also demonstrates the triple works when actually passed: Sandbox.create({ token, teamId, projectId }) succeeded in ~370 ms.

Taken together, three separate codebases end up on the OIDC path unintentionally and then have no way off it in production. That pattern seems worth addressing at the @vercel/oidc level rather than one caller at a time.

Environment

  • @vercel/sandbox 2.7.0, @vercel/oidc (bundled), workflow 5.0.0-beta.46, @ai-sdk/workflow 1.0.30
  • Vercel Functions, production, Node runtime
  • Workload: browser-automation batches, hours-long, one Sandbox session reused across many Workflow steps