Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: support percentage based db limits with reload support by cstockton · Pull Request #2177 · supabase/auth · GitHub
Skip to content

feat: support percentage based db limits with reload support - #2177

Merged
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits
Sep 24, 2025
Merged

feat: support percentage based db limits with reload support#2177
cstockton merged 5 commits into
masterfrom
cs/feat-percentage-based-db-conn-limits

Conversation

@cstockton

@cstocktoncstockton commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Summary

Introduce a context aware DB dial path, a new ConnPercentage knob to cap Auth's share of Postgres connections, and background wiring to apply pool changes on config reloads.

Storage / DB

  • Add DialContext(ctx, *conf.GlobalConfiguration) and keep Dial(...) as a thin wrapper. serve now passes its cancelable context so startup can't hang indefinitely.
  • Connection now keeps a handle to the underlying *sql.DB (via popConnToStd) when available.
  • New helpers:
    • newConnectionDetails and applyDBDriver to build pop.ConnectionDetails and derive driver when omitted.
    • Connection.Copy() to retain sqldb reference and updated locations that copy (WithContext, Transaction).
  • Runtime tuning API: (*Connection).ApplyConfig(ctx, cfg, le) computes and applies connection limits to the underlying *sql.DB.
    • Fixed limits come from MaxPoolSize, MaxIdlePoolSize, ConnMaxLifetime, ConnMaxIdleTime.
    • If ConnPercentage is set (1-100), compute limits from SHOW max_connections, prefer percentage over fixed pool sizes, and set idle = open.
    • Retains previous behavior when ConnPercentage is 0
    • No-op (and error) if *sql.DB is unavailable.

API worker

  • apiworker.New now accepts the DB connection.
  • Split worker into three goroutines (via errgroup):
    • configNotifier fans out reload signals,
    • templateWorker refreshes template cache,
    • dbWorker applies DB connection limits on boot and each reload.

Serve

  • Use storage.DialContext(ctx, cfg) and then db = db.WithContext(ctx) so the DB handle participates in request/trace context and shutdown.

Observability

  • Add observability.NewLogEntry(*logrus.Entry) to construct chi middleware log entries.
  • Structured logs around applying DB limits.

Configuration knobs (GOTRUE_DB_*)

  • GOTRUE_DB_CONN_PERCENTAGE (int, clamped to [0,100]):
    • 0 (default) disables percentage-based sizing.
    • 1-100 reserves that % of max_connections for the Auth server.

Tests

  • internal/storage/dial_test.go
    • DialContext happy path and invalid driver/URL error path.
    • Reflection bridge to *sql.DB (popConnToStd) including WithContext-wrapped connection behavior.
    • ApplyConfig end-to-end: verify pool sizing and stats reflect limits.
    • Percentage math and precedence vs fixed pools across edge cases.
  • internal/conf/configuration_test.go
    • Validation clamps ConnPercentage to [0,100].

How it works

In short if GOTRUE_DB_CONN_PERCENTAGE=0, we use the fixed GOTRUE_DB_{MAX,CONN}_* limits. If it's in the range [1, 100] we set percentage based limits derived from SHOW max_connections and ignore the fixed pool sizes.

Deep Dive

The startup sequence remains the same, trying to set it before we returned from DialContext was a bit messy (chicken / egg: need a conn to setup a conn). I also didn't want to delay startup time during failure scenarios (db is unavailable, db is blocking, etc).

So after DialContext we have a connection which is configured initially with only the existing DB settings:

GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"

Next the server starts the apiworker which immediatelly creates a new dbWorker goroutine concurrently while the rest of the startup sequence continues. Before entering the config update loop the dbWorker will call the newly added ApplyConfig(...) method on the *storage.Connection.

The ApplyConfig method is where the logic for obtaining the best values to call the sql.DB.Set*(...) methods below lives:

Right now ApplyConfig works like this:

  1. Checks that we were able to reflect a *sql.DB during DialContext, if not we do nothing since we can't call sql.DB.Set*(...).

    • If we can't access *sql.DB or fetch max_connections, we leave the prior limits untouched and log a warning. Always all-or-nothing, no partial application of limits.
  2. Calls the new getConnLimits method.

  3. getConnLimits calls newConnLimitsFromConfig which returns a ConnLimits setup with GOTRUE_DB_{MAX,CONN}_* settings.

  4. Check if GOTRUE_DB_CONN_PERCENTAGE is zero, if so it returns the GOTRUE_DB_{MAX,CONN}_* from newConnLimitsFromConfig.

    • This means the limits are set exactly as they are today.
  5. Percentage config is non-zero so we make a call to showMaxConns which just returns an integer from "SHOW max_connections;". In my testing this value always seems to be available for the auth server:

    • This value cannot change without postgres restarts.
    • Postgres will not start if it is 0.
    • Being in recovery mode still shows the maximum connections.
  6. As long as showMaxConns does not return an error we attempt to apply percentage based connection limits in applyPercentageLimits.

  7. If max conns is <= 0 we return an error which prevents any config changes from being applied. Leaving the connection in its prior state.

    • max_connections > 0 is guaranteed if postgres is running, this is a defensive check to prevent applying a clamp to 1 max conns on 0.
  8. We perform a simple bounds check and then set the MaxOpenConns and MaxIdleConns to the values derived from the ConnPercentage and maxConns.

    • Note that we preserve the existing behavior of IdleConns == MaxConns. I believe the aim is to minimize connection churn (latency) at the cost of more Postgres slots when idle. It's worth thinking about making this a bit more considerate in the future, something simple like (open/2) or more advanced heuristics using sql.DBStats.
    pct:=float64(dbCfg.ConnPercentage)
    cl.MaxOpenConns=int(max(1, (pct/100)*float64(maxConns)))
    cl.MaxIdleConns=cl.MaxOpenConns
  9. The values set from the call to getConnLimits are logged before being applied via the sql.DB.Set*(...) calls.

We fail strictly and quickly on derivation errors to keep the last known good settings. By supporting config reloading my hope is that when under high load users may balance this setting without taking down the auth server. This tight feedback loop should help rule out (or resolve) the auth server as a potential root cause to connection timeouts and similar downstream effects.

@stojanapiworker approach also gives a good place for your stats tracking to live, adding a simple ticker in the dbWorker to poll stats between config updates. This stats polling could be used to form additional heuristics in our connect limit tuning if we would like to explore that in the future. For example use the mean connection time as an additional weight to further increase the pool size.

Some notes:

I tested this extensively but please give a thorough review, I made some judgement calls on non-happy paths. I'm also not sure how reliable the sqldb reference is as it seems the composition of the *pop.Store can change based on inputs, context, dialect, driver, etc. The entire feature will not work if I can't reflect out the sqldb.

**Summary**
Introduce a context aware DB dial path, a new `ConnPercentage` knob to cap
Auth's share of Postgres connections, and background wiring to apply pool
changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build `pop.ConnectionDetails`
and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations that
copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes and
applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from
`SHOW max_connections`, prefer percentage over fixed pool sizes, and
set idle = open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db = db.WithContext(ctx)` so
the DB handle participates in request/trace context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi middleware
log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
@cstockton
cstockton requested a review from a team as a code ownerSeptember 22, 2025 23:27
hf
hf approved these changes Sep 23, 2025

@hfhf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the existing DB_MAX_POOL_SIZE setting?

@coveralls

coveralls commented Sep 23, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 17983722060

Details

  • 168 of 237(70.89%) changed or added relevant lines in 4 files are covered.
  • 5 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.1%) to 67.738%

Changes Missing CoverageCovered LinesChanged/Added Lines%
internal/observability/request-logger.go030.0%
internal/storage/dial.go16717794.35%
internal/api/apiworker/apiworker.go0560.0%
Files with Coverage ReductionNew Missed Lines%
internal/api/apiworker/apiworker.go10.0%
internal/storage/dial.go289.01%
internal/tokens/service.go273.47%
TotalsCoverage Status
Change from base Build 17980540058:0.1%
Covered Lines:13240
Relevant Lines:19546

💛 - Coveralls

Include the configuration values, server reported max conns and
the applied limits with `limit_strategy` field describing if
`fixed` or `percentage` was used.
@cstockton
cstocktonforce-pushed the cs/feat-percentage-based-db-conn-limits branch from 4b5bc8d to 21a9928CompareSeptember 23, 2025 17:21
@cstockton

Copy link
Copy Markdown
ContributorAuthor

@stojan updated the PR description to include how the limits work. I also just made a commit with better logging as well.

@cstockton
cstockton merged commit 1731466 into masterSep 24, 2025
5 checks passed
@cstockton
cstockton deleted the cs/feat-percentage-based-db-conn-limits branch September 24, 2025 17:33
fadymak pushed a commit that referenced this pull request Sep 30, 2025
## Summary
Introduce a context aware DB dial path, a new `ConnPercentage` knob to
cap Auth's share of Postgres connections, and background wiring to apply
pool changes on config reloads.
**Storage / DB**
- Add `DialContext(ctx, *conf.GlobalConfiguration)` and keep `Dial(...)`
as a thin wrapper. `serve` now passes its cancelable context so startup
can't hang indefinitely.
- `Connection` now keeps a handle to the underlying `*sql.DB` (via
`popConnToStd`) when available.
- New helpers:
- `newConnectionDetails` and `applyDBDriver` to build
`pop.ConnectionDetails` and derive driver when omitted.
- `Connection.Copy()` to retain `sqldb` reference and updated locations
that copy (`WithContext, Transaction)`.
- Runtime tuning API: `(*Connection).ApplyConfig(ctx, cfg, le)` computes
and applies connection limits to the underlying `*sql.DB`.
- Fixed limits come from `MaxPoolSize`, `MaxIdlePoolSize`,
`ConnMaxLifetime`, `ConnMaxIdleTime`.
- If `ConnPercentage` is set (1-100), compute limits from `SHOW
max_connections`, prefer percentage over fixed pool sizes, and set idle
= open.
- Retains previous behavior when `ConnPercentage` is `0`
- No-op (and error) if `*sql.DB` is unavailable.
**API worker**
- `apiworker.New` now accepts the DB connection.
- Split worker into three goroutines (via `errgroup`):
- `configNotifier` fans out reload signals,
- `templateWorker` refreshes template cache,
- `dbWorker` applies DB connection limits on boot and each reload.
**Serve**
- Use `storage.DialContext(ctx, cfg)` and then `db =
db.WithContext(ctx)` so the DB handle participates in request/trace
context and shutdown.
**Observability**
- Add `observability.NewLogEntry(*logrus.Entry)` to construct chi
middleware log entries.
- Structured logs around applying DB limits.
**Configuration knobs** (`GOTRUE_DB_*`)
- `GOTRUE_DB_CONN_PERCENTAGE` (int, clamped to `[0,100]`):
- `0` (default) disables percentage-based sizing.
- `1-100` reserves that % of `max_connections` for the Auth server.
**Tests**
- `internal/storage/dial_test.go`
- `DialContext` happy path and invalid driver/URL error path.
- Reflection bridge to `*sql.DB` (`popConnToStd`) including
`WithContext`-wrapped connection behavior.
- `ApplyConfig` end-to-end: verify pool sizing and stats reflect limits.
- Percentage math and precedence vs fixed pools across edge cases.
- `internal/conf/configuration_test.go`
- Validation clamps `ConnPercentage` to `[0,100]`.
## How it works
In short if `GOTRUE_DB_CONN_PERCENTAGE=0`, we use the fixed
`GOTRUE_DB_{MAX,CONN}_*` limits. If it's in the range `[1, 100]` we set
percentage based limits derived from `SHOW max_connections` and ignore
the fixed pool sizes.
### Deep Dive
The startup sequence remains the same, trying to set it _before_ we
returned from `DialContext` was a bit messy (chicken / egg: need a conn
to setup a conn). I also didn't want to delay startup time during
failure scenarios (db is unavailable, db is blocking, etc).
So after `DialContext` we have a connection which is configured
initially with only the existing DB settings:
```bash
GOTRUE_DB_MAX_POOL_SIZE="50"
GOTRUE_DB_MAX_IDLE_POOL_SIZE="10"
GOTRUE_DB_CONN_MAX_IDLE_TIME="60s"
GOTRUE_DB_CONN_MAX_LIFETIME="0"
```
Next the server starts the `apiworker` which immediatelly creates a new
[dbWorker](https://github.com/supabase/auth/pull/2177/files#diff-b20c1e9d1c21d077494cf5ff490de301a864d3d1812538cf594a687f620a7175R122)
goroutine concurrently while the rest of the startup sequence continues.
Before entering the config update loop the `dbWorker` will call the
newly added
[ApplyConfig(...)](https://github.com/supabase/auth/pull/2177/files#diff-5b7e4f0f03bfbc3a58168e58eb88386b9e683241c1ebcb57f6764c38308f2257R179)
method on the `*storage.Connection`.
The `ApplyConfig` method is where the logic for obtaining the best
values to call the `sql.DB.Set*(...)` methods below lives:
* [SetConnMaxIdleTime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxIdleTime)
* [SetConnMaxLifetime(d
time.Duration)](https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime)
* [SetMaxIdleConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxIdleConns)
* [SetMaxOpenConns(n
int)](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns)
Right now
[ApplyConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L179)
works like this:
1. Checks that we were able to
[reflect](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L140)
a `*sql.DB` during `DialContext`, if not we do nothing since we can't
call `sql.DB.Set*(...)`.
* If we can't access *sql.DB or fetch max_connections, we leave the
prior limits untouched and log a warning. Always all-or-nothing, no
partial application of limits.
2. Calls the new
[getConnLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L209)
method.
3. `getConnLimits` calls
[newConnLimitsFromConfig](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L287)
which returns a `ConnLimits` setup with `GOTRUE_DB_{MAX,CONN}_*`
settings.
4. Check if `GOTRUE_DB_CONN_PERCENTAGE` is zero, if so [it
returns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L218)
the `GOTRUE_DB_{MAX,CONN}_*` from `newConnLimitsFromConfig`.
* This means the limits are set exactly as they are today.
5. Percentage config is non-zero so we make a call to
[showMaxConns](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L266)
which just returns an integer from `"SHOW max_connections;"`. In my
testing this value always seems to be available for the auth server:
* This value cannot change without postgres restarts.
* Postgres will not start if it is 0.
* Being in recovery mode still shows the maximum connections.
6. As long as `showMaxConns` does not return an error we attempt to
apply percentage based connection limits in
[applyPercentageLimits](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L235).
7. If max conns is <= 0 we return [an
error](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L245)
which prevents any config changes from being applied. Leaving the
connection in its prior state.
* max_connections > 0 is guaranteed if postgres is running, this is a
defensive check to prevent applying a clamp to 1 max conns on 0.
8. We perform a simple [bounds
check](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L252)
and then set the [`MaxOpenConns` and
`MaxIdleConns`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L257)
to the values derived from the `ConnPercentage` and `maxConns`.
* Note that we preserve the existing behavior of IdleConns == MaxConns.
I believe the aim is to minimize connection churn (latency) at the cost
of more Postgres slots when idle. It's worth thinking about making this
a bit more considerate in the future, something simple like (open/2) or
more advanced heuristics using
[sql.DBStats](https://pkg.go.dev/database/sql#DBStats).
```Go
pct := float64(dbCfg.ConnPercentage)
cl.MaxOpenConns = int(max(1, (pct/100)*float64(maxConns)))
cl.MaxIdleConns = cl.MaxOpenConns
```
9. The values set from the call to `getConnLimits` are logged before
being [applied via the
`sql.DB.Set*(...)`](https://github.com/supabase/auth/blob/4b5bc8d08fb4fbdf778f504f226da29246d34e84/internal/storage/dial.go#L202)
calls.
We fail strictly and quickly on derivation errors to keep the last known
good settings. By supporting config reloading my hope is that when under
high load users may balance this setting without taking down the auth
server. This tight feedback loop should help rule out (or resolve) the
auth server as a potential root cause to connection timeouts and similar
downstream effects.
@stojan `apiworker` approach also gives a good place for your [stats
tracking](#2167) to live, adding a
simple ticker in the `dbWorker` to poll stats between config updates.
This stats polling could be used to form additional heuristics in our
connect limit tuning if we would like to explore that in the future. For
example use the mean connection time as an additional weight to further
increase the pool size.
## Some notes:
I tested this extensively but please give a thorough review, I made some
judgement calls on non-happy paths. I'm also not sure how reliable the
sqldb reference is as it seems the composition of the *pop.Store can
change based on inputs, context, dialect, driver, etc. The entire
feature will not work if I can't reflect out the sqldb.
---------
Co-authored-by: Chris Stockton <chris.stockton@supabase.io>
fadymak pushed a commit that referenced this pull request Nov 4, 2025
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([#2197](#2197))
([9a8d0df](9a8d0df))
* add `auth_migration` annotation for the migrations
([#2234](#2234))
([b276d0b](b276d0b))
* add advisor to notify you when to double the max connection pool
([#2167](#2167))
([a72f5d9](a72f5d9))
* add after-user-created hook
([#2169](#2169))
([bd80df8](bd80df8))
* add support for account changes notifications in email send hook
([#2192](#2192))
([6b382ae](6b382ae))
* email address changed notification
([#2181](#2181))
([047f851](047f851))
* identity linked/unlinked notifications
([#2185](#2185))
([7d46936](7d46936))
* introduce v2 refresh token algorithm
([#2216](#2216))
([dea5b8e](dea5b8e))
* MFA factor enrollment notifications
([#2183](#2183))
([53db712](53db712))
* notify users when their phone number has changed
([#2184](#2184))
([21f3070](21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([#2231](#2231))
([6296a5a](6296a5a))
* properly handle redirect url fragments and unusual hostnames
([#2200](#2200))
([aa0ac5b](aa0ac5b))
* store latest challenge/attestation data
([#2179](#2179))
([01ebce1](01ebce1))
* support percentage based db limits with reload support
([#2177](#2177))
([1731466](1731466))
* webauthn support schema changes, update openapi.yaml
([#2163](#2163))
([68cb8d2](68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([#2222](#2222))
([bca6626](bca6626))
* **openapi:** add missing OAuth client registration fields
([#2227](#2227))
([cf39a8a](cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
DevRyuki pushed a commit to sasatech-gk/supabase-auth that referenced this pull request Feb 23, 2026
🤖 I have created a release *beep* *boop*
---
##
[2.181.0](supabase/auth@v2.180.0...v2.181.0)
(2025-10-31)
### Features
* add `.well-known/openid-configuration`
([supabase#2197](supabase#2197))
([9a8d0df](supabase@9a8d0df))
* add `auth_migration` annotation for the migrations
([supabase#2234](supabase#2234))
([b276d0b](supabase@b276d0b))
* add advisor to notify you when to double the max connection pool
([supabase#2167](supabase#2167))
([a72f5d9](supabase@a72f5d9))
* add after-user-created hook
([supabase#2169](supabase#2169))
([bd80df8](supabase@bd80df8))
* add support for account changes notifications in email send hook
([supabase#2192](supabase#2192))
([6b382ae](supabase@6b382ae))
* email address changed notification
([supabase#2181](supabase#2181))
([047f851](supabase@047f851))
* identity linked/unlinked notifications
([supabase#2185](supabase#2185))
([7d46936](supabase@7d46936))
* introduce v2 refresh token algorithm
([supabase#2216](supabase#2216))
([dea5b8e](supabase@dea5b8e))
* MFA factor enrollment notifications
([supabase#2183](supabase#2183))
([53db712](supabase@53db712))
* notify users when their phone number has changed
([supabase#2184](supabase#2184))
([21f3070](supabase@21f3070))
* **oauthserver:** add OAuth client admin update endpoint
([supabase#2231](supabase#2231))
([6296a5a](supabase@6296a5a))
* properly handle redirect url fragments and unusual hostnames
([supabase#2200](supabase#2200))
([aa0ac5b](supabase@aa0ac5b))
* store latest challenge/attestation data
([supabase#2179](supabase#2179))
([01ebce1](supabase@01ebce1))
* support percentage based db limits with reload support
([supabase#2177](supabase#2177))
([1731466](supabase@1731466))
* webauthn support schema changes, update openapi.yaml
([supabase#2163](supabase#2163))
([68cb8d2](supabase@68cb8d2))
### Bug Fixes
* gosec incorrectly warns about accessing signature[64]
([supabase#2222](supabase#2222))
([bca6626](supabase@bca6626))
* **openapi:** add missing OAuth client registration fields
([supabase#2227](supabase#2227))
([cf39a8a](supabase@cf39a8a))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@cstockton@coveralls@hf