If server code that calls Supabase with one of the new sb_secret_ keys sometimes gets back HTTP 401 with code PGRST303 and the message "JWT issued at future", the fault is almost certainly not your clock and not your code. PostgREST, the server behind Supabase's Data API, kept a cached copy of the current time in its recent releases, and that copy could go stale, so a token minted a moment earlier looked as if it came from the future. PostgREST fixed it in version 14.18 on September 10. Supabase's own status page traced its incident to "a stale time cache" and, on September 29, told affected users to upgrade their projects from the dashboard. Most projects that gave their version when reporting the error in October and November were on PostgREST 14.5; one was on 14.15. SaasBuzz's own Supabase project, created in August, was serving PostgREST 14.5 too when we checked on November 3.
Our take: check which PostgREST version is actually serving your project, and if it is older than 14.18 (and is not Supabase's patched 14.5.1), schedule Supabase's "Upgrade project" in a maintenance window, because that is the only route to the fixed code that Supabase has named. Swapping your server back to the legacy service_role key is a measured stopgap that stopped the errors for at least one team, but it belongs only on server paths that cannot fail, and it comes with an expiry date: Supabase is deprecating that key by the end of 2026. Retries alone are not a plan, since some of the clock gaps run far longer than any sensible retry.
The reason you see a JWT error when you never send a JWT: Supabase's API keys guide says the new keys "are short strings, not JWTs". Reporters who read their projects' edge logs found no caller token on the failing requests, which, as they read it, means the token PostgREST rejected was one Supabase's gateway created from the key. Supabase has not documented that step, but the error message itself shows that PostgREST was handed a token to check. Threads and versions below were checked on November 3, 2026.
The response body looks like this:
{
"code": "PGRST303",
"details": null,
"hint": null,
"message":
"JWT issued at future"
}How to make it stop, in order
Log the full error. PostgREST uses PGRST303 for any failed token claim check, according to its error reference, so "JWT expired" is a different problem from "JWT issued at future".
Find the PostgREST version that is serving requests, with the SQL below, not only the number on the dashboard.
If it is below 14.18 on the 14 line (14.5.1, Supabase's own patched build, is the exception), or below 16.3 on the 16 line, plan "Upgrade project" for a quiet hour. It takes the project offline.
Until then, retry reads that hit this exact error, and move server code that cannot tolerate a failure to the legacy
service_rolekey if your project still has it, with a date to move back.
Is it my server's clock, or Vercel's?
The reports in Supabase's GitHub issues point away from both. One reporter on issue #50651 measured the project's Postgres clock at 0.20 seconds from theirs and the edge's Date header within a second, and saw the error from Vercel's iad1 region and from a laptop alike. Another moved their Vercel functions to Frankfurt, next to their project in eu-central-1, and the error kept arriving through Frankfurt and Amsterdam, according to issue #49655. Reports in the two issues come from Vercel, Netlify, Railway and local machines. PostgREST already allows "a 30-second clock skew" on the iat, exp and nbf claims, its authentication docs say, so a few seconds of drift on your side would not produce this error.
The strongest case for the clock theory came from inside PostgREST. Maintainer Wolfgang Walther argued in PostgREST issue #5196 that a PostgREST bug was "very unlikely" and asked whether anyone outside Supabase had seen it. That objection did not hold. A team running PostgREST on its own virtual machines, with clocks it had checked, reported the same error after long idle periods, with PostgREST's logs showing a timestamp two weeks old, and Walther called it "massively helpful". Maintainer Steve Chavez reproduced the error with both machines' clocks in sync, and his fix, pull request #5208, removed the cached time and reads the system clock directly.

Your clock does matter in one case: if you mint tokens yourself or use a third-party auth provider, the issuer's clock sets iat. Check it, then look at PostgREST.
How it unfolded
Date | What happened |
|---|---|
Jul. 21 | Earliest report found, on sign-in tokens |
Aug. 14 | Supabase opens a 401 incident |
Aug. 26 | PostgREST opens #5196 on 14.17 |
Aug. 27 | Supabase: 14.17 in all regions |
Aug. 31 | Supabase rolls back to 14.5 |
Sep. 2 | Fix merged; Supabase names a stale time cache |
Sep. 10 | PostgREST 14.18 ships the fix |
Sep. 29 | Supabase closes the incident: upgrade |
Oct. 5 | One project: every secret-key request fails |
Nov. 2 | A hosted project still reports 14.5 |
Nov. 3 | Our own project: 14.5 |
Sources: Supabase discussion #48123, the Supabase status incident, PostgREST #5196 and its releases, Supabase issues #50651 and #52541, and our own check. Dates UTC.
The rollback explains the version number. Supabase wrote on August 31 that the rejections "persisted with 14.17" and that, after "some unintended performance side effects", it had "rolled back to Postgrest 14.5". Two days later it named the cause, and on September 17 it said its fleet needed "a change to our deploy process", adding: "We recognize that this is added work for our customers." Since then, getting the fix has been each customer's own step. By our count, 54 days have passed since PostgREST shipped 14.18 and 35 since Supabase closed its incident, and projects on 14.5 are still filing issues.

Which PostgREST version is my project really on?
The dashboard shows service versions under Settings, General, according to Supabase's upgrading guide. Do not stop there. The reporter on issue #52541 found the dashboard showing PostgREST 14.17 while a live PostgREST connection identified itself as 14.5. PostgREST names its own database connections, and its connection pool docs give this query, which you can run in the SQL editor:
select distinct
usename,
application_name
from pg_stat_activity
where usename =
'authenticator';The application_name column carries the version. The fix is in 14.18 and 16.3, according to both release notes, and in 14.5.1, a build Supabase tagged in its own PostgREST fork on September 2 with the same fix. Plain 14.5 does not have it, and 14.17 is not enough: it carried an earlier attempt that PostgREST's own issue calls "ineffective".
We ran the check on our own project
SaasBuzz runs on hosted Supabase, so we tried the check on ourselves. On November 3 at 10:56 UTC we ran that exact query on SaasBuzz's production project, reading only, and changed nothing. It returned one row: usename "authenticator", application_name "PostgREST 14.5". A second query, select version(), reported PostgreSQL 17.6.
Check | Our project |
|---|---|
PostgREST | 14.5 |
Postgres | 17.6 |
Created | Aug. 19, 2026 |
Region | us-west-1 |
Sources: SaasBuzz's queries on its own Supabase project, November 3, 2026, 10:56 UTC (versions), and the project's own record (creation date, region).
What it means: our project was created on August 19, five days after Supabase opened its incident, and 76 days later it serves from plain 14.5, the build without the fix. The 35 days since Supabase closed the incident did not move it onto fixed code. Its Postgres, 17.6, is the release a later commenter on #49655 gave for a 14.5 project, and the one a discussion #48123 user upgraded from before seeing 14.18. If you have not run "Upgrade project" since September 29, do not assume you have the fix: run the query.
We do not see the error ourselves. SaasBuzz does not use Supabase's Data API, which is closed on our project, so we send no requests that could hit it. A project on our version that does use the Data API with sb_secret_ keys is running the code without the fix.
How do I get the fixed version?
Supabase's answer, from its status page on September 29: "All users impacted by this issue can now upgrade from the dashboard to resolve this issue."

The button is "Upgrade project" on the General settings page. The upgrading guide says the project "is taken offline", that a new instance is created and the data copied to it, that other services are upgraded "at the same time", and that the downtime depends on the size of the database. Projects with read replicas, logical replication slots or deprecated extensions must deal with those first.
Two cautions. One reporter wrote on issue #49655 that an upgrade "does not change the PostgREST version", which contradicts Supabase's status page. Two later posts in discussion #48123 point the other way: on October 24 one user wrote "I upgraded a project to latest Postgres and get 14.18 PostgREST", and the next day another wrote that upgrading Postgres from 17.6 to 17.11 "also upgraded PostgREST from 14.15 to 14.18". And the reporter on #52541 asked Supabase on November 2 whether "Upgrade project" would reach a build with the fix, with no answer from Supabase in the issue when we checked. So upgrade, then run the query above again and confirm 14.18 or later before you call it done.
Restarting the project is not a fix. Supabase's status page said in August that "some customers" reported a restart helped after its rollout, and one reporter on #50651 wrote that a restart seems "to rotate the affected instance out temporarily rather than fix it." We have not verified whether a restart helps on 14.5. Several reporters opened support tickets; none has posted the outcome.
Should I go back to the legacy service_role key?
The evidence for the swap is one team's production data. On issue #50651, a team running supabase-js on Netlify Functions logged 26 refusals out of about 932 requests in a day, 2.8%, then switched one server-side key to the legacy service_role JWT on September 27 and saw "zero PGRST303 401s in 13+ hours". At the rate it had measured over one day of traffic before the switch, about 1.1 refusals an hour by our count (the team put it at about 1.25), some 14 would have been expected in that time, so a clean run by chance is very unlikely, though the errors come in bursts, which weakens that arithmetic. Another reporter's legacy key passed 20 of 20 probes, and a third, whose secret key failed on every request, got 200 responses again "right after the switch".

Option | Stops it? | Cost |
|---|---|---|
Upgrade project | Supabase says yes | Downtime |
Legacy key | Yes, for one team | Deprecated end of 2026 |
Retry | Short gaps only | Latency |
Restart | Briefly, per reporters | Short outage |
Sources: Supabase status incident, issue #50651, Supabase's API keys and upgrading guides.
The trade-offs come from Supabase's API keys guide. Both keys bypass Row Level Security, so the swap does not widen what the server can reach. But the new secret keys add checks the old one lacks: a secret key "doesn't work in a browser", for one. The legacy key is a long-lived JWT, and Supabase plans to retire it by the end of 2026. So swap only server code where a 401 does real damage, such as a checkout or a public page that turns into an error, keep the key out of anything that ships to users, and write down the day you will move back. If your rate looks like the 1 in 477 another reporter measured, and the failing reads can fall back to cached data, retry and upgrade instead.
Why does it fail right after sign-in?
The same check catches user tokens. On issue #52541, a user signed in with signInWithPassword(), got a session, and the very next Data API call failed with "JWT issued at future"; discussion #48123 describes the same with fresh sessions. A freshly minted token carries an iat of "now", and a PostgREST reading an old cached time sees it as early. Swapping keys does nothing here, because the rejected token comes from Supabase Auth. The #52541 reporter said "a bounded retry did not resolve it", so for sign-in failures the upgrade is the fix.
A retry that also records the evidence
This wrapper, for supabase-js v2, retries only this exact error, builds a fresh query each time, and logs the status and the UTC time of each refusal it retries, which is what you will want for a support ticket. We type-checked it against supabase-js 2.118.0 and ran it against a simulated 401 response.
import {
createClient,
type PostgrestError,
} from '@supabase/supabase-js'
const env = process.env
const supabase = createClient(
env.SUPABASE_URL!,
env.SUPABASE_SECRET_KEY!,
)
const isFuture = (
e: PostgrestError | null,
) =>
e?.code === 'PGRST303' &&
e.message ===
'JWT issued at future'
const wait = (ms: number) =>
new Promise((r) =>
setTimeout(r, ms))
// Reads and idempotent RPCs:
// retry after 0.5 s, 2 s, 8 s.
async function retry<R extends {
error: PostgrestError | null
status: number
}>(run: () => PromiseLike<R>) {
let res = await run()
for (
let n = 0;
n < 3 && isFuture(res.error);
n++
) {
console.warn(
'PGRST303',
res.status,
new Date().toISOString(),
)
await wait(500 * 4 ** n)
res = await run()
}
return res
}
// A fresh query each attempt.
const { data, error } =
await retry(() =>
supabase
.from('events')
.select('id, name'),
)
if (error) {
console.error(
error.code,
error.message,
)
}Know its limit. The first report recovered on a retry about 1.4 seconds later, but a team on #50651 had one refusal outlast a retry budget of about 2 seconds, and in Chavez's reproduction on a test build on September 1 the token was 8 to 174 seconds ahead of PostgREST's cached clock. A 10.5-second budget catches the short blips, not the long ones. Never log the key itself; Supabase's guide says not to, "even one that looks invalid".
When it is probably something else
One report does not fit the pattern. On #50651, a team said every request with its secret key had failed since October 5 at 20:47 UTC, about 1,900 a day for seven days, while the publishable key worked on the same project. A stale cache that comes and goes does not obviously explain a week of total failure, and we could not establish another cause; the legacy key worked for them too. If your failure rate is 100%, treat the version check as a first step, not the answer, and give Supabase the failing request times.
A message of "JWT expired" is a different claim failing, and on an earlier report, issue #41294, a Supabase contributor asked whether an old stored token was to blame.
Sources
PostgREST issue #5196 and pull request #5208
Supabase status page, "401 errors due to JWT rejections", August 14 to September 29
Supabase issues #49655, #50651 and #52541; discussion #48123
Supabase docs: API keys; Upgrading
PostgREST docs: errors, authentication, connection pool
SaasBuzz's read-only queries on its own Supabase project, November 3, 2026






