# 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.

Source: https://supahost.app/kb/jwt-issued-at-future · Updated 2026-11-11

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/`:

```json
{"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:

   ```js
   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:

   ```js
   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:

   ```js
   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.

## Related

- [JWT expired](/kb/jwt-expired)
- [Sessions, JWTs and refresh tokens](/docs/auth/sessions)
- [HTTP status codes from the Supabase APIs](/kb/http-status-codes)
- [401 invalid claim: missing sub claim](/kb/invalid-claim-missing-sub)
