Summary
On a hosted Pro project, the platform rendered the stored sessions_timebox value into
GoTrue's environment with an h suffix instead of s. The stored value is 2592000
— thirty days in seconds, which is the unit the Management API documents and returns.
The container therefore received GOTRUE_SESSIONS_TIMEBOX=2592000h, which overflows Go's
time.Duration, and GoTrue refused to boot:
level=fatal msg="Failed to load configuration: envconfig.Process: assigning
GOTRUE_SESSIONS_TIMEBOX to Timebox: converting '2592000h' to type *time.Duration.
details: time: invalid duration \"2592000h\""
This repeated roughly every 3.2 seconds — 6,539 occurrences — and every /auth/v1/*
call returned 503 upstream connect error ... delayed connect error: 111 for six hours.
Postgres and PostgREST were unaffected (GET /rest/v1/ → 401, i.e. healthy), so this was
auth-only: no user on the project could sign in.
The value had been set on 2026-08-11 and the project ran normally for twenty days on
v2.195.0. The restart that broke it was a version upgrade: the instance came back as
v2.196.0, and that is the boot at which 2592000h first appears in the logs. GoTrue reads
its config only at process start, so whatever changed in how the platform renders this key,
nothing could surface until the next restart — which was scheduled by the platform, not by
the project owner.
That is the part worth designing against. A configuration value that is only validated at
boot is a value that can sit armed for weeks behind a green dashboard, a green config API,
and a green config-drift check, and then detonate on a maintenance window nobody chose.
The wider problem: below the overflow threshold there is no error at all
The crash is the lucky branch.
time.Duration is int64 nanoseconds, so the ceiling is (2^63−1)/1e9/3600 =
2,562,047.79 h. 2592000h exceeds it by 1.17% — which is the only reason this
project got a loud failure instead of a silent one.
Any project whose sessions_timebox is below ~2,562,047 seconds (≈29.65 days) boots
cleanly and silently applies a value multiplied by 3600:
| stored (seconds) |
intended |
actually applied if rendered as h |
3600 |
1 hour |
3600h — 150 days |
86400 |
1 day |
86400h — 9.86 years |
604800 |
7 days |
604800h — 69 years |
2592000 |
30 days |
overflow → crash loop |
That is a security control silently disabled — sessions that should time-box do not — with
no error, no log line, and nothing in the dashboard to indicate it. Far more projects are
likely affected that way than by the crash, and by construction none of them can tell.
Expected behaviour
- The control plane should render
sessions_timebox in the unit it stores (seconds), or
convert it, so GoTrue receives a duration equal to the configured value.
- Independently of (1),
auth should not be able to enter an unrecoverable boot loop on
an out-of-range duration. Two options that would each have prevented six hours of
downtime here: reject the value at config-write time (so the dashboard/API refuses it
while the service is still up), or clamp to the maximum representable duration and log
a warn rather than fatal. A config value that is only validated at boot is a value
that can sit armed for weeks.
Reproduction
- On a hosted project,
PATCH /v1/projects/{ref}/config/auth with
{"sessions_timebox": 2592000} (or set Auth → Sessions → Time-box user sessions).
- Confirm
GET /v1/projects/{ref}/config/auth returns sessions_timebox: 2592000.
- Trigger (or wait for) a GoTrue restart.
GET /auth/v1/health with an apikey header → 503. Auth logs show the fatal line
above, repeating.
Diagnostic note for whoever picks this up
A keyless GET /auth/v1/health returns 401 while GoTrue is completely dead — the
gateway rejects the request before it reaches the service, so the response is identical to
the healthy case. Any external uptime check that cannot set an apikey header is
structurally incapable of detecting this outage. That may be worth documenting.
Workaround
Set the value so that stored × 1 hour equals the intended duration (30 days → 720), or
set 0 to disable the time-box entirely.
Resolution on our side, and what it confirms
Setting sessions_timebox: 720 via the Management API restored the service immediately:
fatal count: 19/min at 05:01:50 UTC → 0 from 05:02, first GoTrue API started at 05:01:54.
/auth/v1/health (with apikey): 503 → 200, reporting v2.196.0.
- Total outage: 7 h 29 m.
- A partial
PATCH carrying only that key changed exactly 1 of 242 config keys (diffed
against a GET captured beforehand).
It also gives an observational read on the unit, without waiting 30 days: a session created
7.51 days earlier refreshed successfully 6 seconds after the service came back. GoTrue
enforces the time-box as created_at + timebox at refresh, so a 720 interpreted as seconds
(12 min) or minutes (12 h) would have terminated that session. It did not — consistent with
720h, and confirming the integer is being read as hours.
Environment
- Hosted Supabase, Pro plan,
us-west — project ref available on request / also filed via support
- GoTrue version reported by
/auth/v1/health before the outage: v2.195.0
- First
fatal: 2026-08-31 21:32:48 UTC · last successful /token: 2026-08-31 18:53:22 UTC
Summary
On a hosted Pro project, the platform rendered the stored
sessions_timeboxvalue intoGoTrue's environment with an
hsuffix instead ofs. The stored value is2592000— thirty days in seconds, which is the unit the Management API documents and returns.
The container therefore received
GOTRUE_SESSIONS_TIMEBOX=2592000h, which overflows Go'stime.Duration, and GoTrue refused to boot:This repeated roughly every 3.2 seconds — 6,539 occurrences — and every
/auth/v1/*call returned
503 upstream connect error ... delayed connect error: 111for six hours.Postgres and PostgREST were unaffected (
GET /rest/v1/→401, i.e. healthy), so this wasauth-only: no user on the project could sign in.
The value had been set on 2026-08-11 and the project ran normally for twenty days on
v2.195.0. The restart that broke it was a version upgrade: the instance came back asv2.196.0, and that is the boot at which2592000hfirst appears in the logs. GoTrue readsits config only at process start, so whatever changed in how the platform renders this key,
nothing could surface until the next restart — which was scheduled by the platform, not by
the project owner.
That is the part worth designing against. A configuration value that is only validated at
boot is a value that can sit armed for weeks behind a green dashboard, a green config API,
and a green config-drift check, and then detonate on a maintenance window nobody chose.
The wider problem: below the overflow threshold there is no error at all
The crash is the lucky branch.
time.Durationis int64 nanoseconds, so the ceiling is(2^63−1)/1e9/3600=2,562,047.79 h.
2592000hexceeds it by 1.17% — which is the only reason thisproject got a loud failure instead of a silent one.
Any project whose
sessions_timeboxis below ~2,562,047 seconds (≈29.65 days) bootscleanly and silently applies a value multiplied by 3600:
h3600864006048002592000That is a security control silently disabled — sessions that should time-box do not — with
no error, no log line, and nothing in the dashboard to indicate it. Far more projects are
likely affected that way than by the crash, and by construction none of them can tell.
Expected behaviour
sessions_timeboxin the unit it stores (seconds), orconvert it, so GoTrue receives a duration equal to the configured value.
authshould not be able to enter an unrecoverable boot loop onan out-of-range duration. Two options that would each have prevented six hours of
downtime here: reject the value at config-write time (so the dashboard/API refuses it
while the service is still up), or clamp to the maximum representable duration and log
a
warnrather thanfatal. A config value that is only validated at boot is a valuethat can sit armed for weeks.
Reproduction
PATCH /v1/projects/{ref}/config/authwith{"sessions_timebox": 2592000}(or set Auth → Sessions → Time-box user sessions).GET /v1/projects/{ref}/config/authreturnssessions_timebox: 2592000.GET /auth/v1/healthwith anapikeyheader →503. Auth logs show thefatallineabove, repeating.
Diagnostic note for whoever picks this up
A keyless
GET /auth/v1/healthreturns 401 while GoTrue is completely dead — thegateway rejects the request before it reaches the service, so the response is identical to
the healthy case. Any external uptime check that cannot set an
apikeyheader isstructurally incapable of detecting this outage. That may be worth documenting.
Workaround
Set the value so that
stored × 1 hourequals the intended duration (30 days →720), orset
0to disable the time-box entirely.Resolution on our side, and what it confirms
Setting
sessions_timebox: 720via the Management API restored the service immediately:fatalcount: 19/min at 05:01:50 UTC → 0 from 05:02, firstGoTrue API startedat 05:01:54./auth/v1/health(withapikey): 503 → 200, reportingv2.196.0.PATCHcarrying only that key changed exactly 1 of 242 config keys (diffedagainst a
GETcaptured beforehand).It also gives an observational read on the unit, without waiting 30 days: a session created
7.51 days earlier refreshed successfully 6 seconds after the service came back. GoTrue
enforces the time-box as
created_at + timeboxat refresh, so a720interpreted as seconds(12 min) or minutes (12 h) would have terminated that session. It did not — consistent with
720h, and confirming the integer is being read as hours.Environment
us-west— project ref available on request / also filed via support/auth/v1/healthbefore the outage:v2.195.0fatal: 2026-08-31 21:32:48 UTC · last successful/token: 2026-08-31 18:53:22 UTC