PGRST303: JWT issued at future

Why the Data API rejects a token with "JWT issued at future", how PostgREST checks the iat claim with 30 seconds of tolerance, and how to find the token's real source.

PostgREST refuses a token whose iat (issued at) time is later than its own clock. It allows 30 seconds of drift, so this error means the token claims to have been issued well in the future.

What you see

HTTP 401 from /rest/v1/:

{"code":"PGRST303","details":null,"hint":null,"message":"JWT issued at future"}

The response also carries a WWW-Authenticate: Bearer error="invalid_token" header. Related messages from the same check are JWT expired (the exp claim has passed) and JWT not yet valid (nbf).

Why it happens

PostgREST compares three time claims with the current time on the server: exp, nbf and iat. For iat it rejects the token if iat is more than 30 seconds ahead of its clock. That leaves two possibilities:

  1. Whoever signed the token has a clock that's ahead. This can only happen when the token is signed on a different machine from the one checking it.
  2. The iat value is wrong. The classic bug is code that signs its own tokens with iat: Date.now(). JWT times are in seconds, Date.now() is in milliseconds, and the result is a date thousands of years away.

How to fix it

  1. Decode the token and compare iat with the current time. In a browser console or Node:

    const token = (await supabase.auth.getSession()).data.session?.access_token
    const payload = JSON.parse(
      atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
    )
    console.log('iat', payload.iat, 'now', Math.floor(Date.now() / 1000), 'iss', payload.iss)
    
  2. If iat is a 13-digit number, it's in milliseconds. Fix the code that signs it:

    const now = Math.floor(Date.now() / 1000)
    const claims = { sub: userId, role: 'authenticated', aud: 'authenticated', iat: now, exp: now + 3600 }
    
  3. If iat is a few minutes ahead, look at the issuer (iss). A token from your own server or another system means that machine's clock is wrong. Turn on NTP there (timedatectl set-ntp true on most Linux hosts).

  4. Get a fresh session once the source is fixed, so the client stops sending the bad token:

    await supabase.auth.refreshSession()
    

On SupaHost

A project's Auth and Data API run on the same VM, so access tokens from SupaHost Auth are checked against the same clock that issued them. If you see PGRST303 on SupaHost, the token almost always came from somewhere else: your own signing code or another identity system. You can confirm by checking iss: tokens from a project's Auth use https://<project host>/auth/v1, for example https://app-one.acme.supahost.space/auth/v1.

A device clock that's wrong doesn't cause this error, because the server does the check. It can make supabase-js refresh tokens at odd times, though, so it's worth fixing on test devices.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us