Hey folks! We've been tracking down a memory leak in our production Go service that uses River v0.33+ with riverpgxv5, and we traced it back to the sqlctemplate.Replacer cache never being used for schema replacements.
What's happening
The schemaTemplateParam function in riverpgxv5 passes Stable: false (the default) on the schema replacement:
// riverdriver/riverpgxv5/river_pgx_v5_driver.go
func schemaTemplateParam(ctx context.Context, schema string) context.Context {
return sqlctemplate.WithReplacements(ctx, map[string]sqlctemplate.Replacement{
"schema": {Value: schema}, // Stable defaults to false
}, nil)
}
Because of this, replacerCacheKeyFrom always returns cacheEligible = false, and ReplaceAllStringFunc runs on every single query — even though the schema value never changes for the lifetime of a River client.
The caching mechanism from #802 exists and works correctly, it's just never activated because Stable isn't set.
Impact we observed
We run River in two deployments (an API that enqueues jobs and a worker that processes them). After profiling with go tool pprof in production:
- Worker (runs
river.Start() with continuous polling): regexp.ReplaceAllStringFunc grew +29MB in 16 minutes, accounting for 99% of all memory growth
- API (insert-only client):
regexp.ReplaceAllStringFunc grew +16.9MB in 20 minutes, accounting for 89% of all memory growth
The strings allocated by the regex are short-lived but under production throughput, the allocation rate outpaces GC and downstream consumers (like pgx tracers or OTel span attributes) can hold references long enough for the memory to accumulate.
We confirmed the fix by patching schemaTemplateParam locally via a go.mod replace directive — after deploying with Stable: true, memory graphs in our monitoring flatlined.
Suggested fix
One-line change in schemaTemplateParam:
"schema": {Value: schema, Stable: true},
The same applies to the ColumnExists method which also passes a schema replacement without Stable.
The schema value is derived from the client's config and is constant for the lifetime of the River client — it's the textbook case for Stable: true.
This wouldn't affect queries that use dynamic replacements like where_clause and order_by_clause (e.g., JobList, JobDeleteMany) — those correctly remain uncached since their replacements don't set Stable.
Environment
- River: v0.33.0 (also confirmed on v0.35.1)
- riverpgxv5
- Go 1.26.2
- pgx v5.9.2
Happy to open a PR if you'd like!
Screenshot of our pods in sandbox environment before and after the fix

Hey folks! We've been tracking down a memory leak in our production Go service that uses River v0.33+ with
riverpgxv5, and we traced it back to thesqlctemplate.Replacercache never being used for schema replacements.What's happening
The
schemaTemplateParamfunction inriverpgxv5passesStable: false(the default) on the schema replacement:Because of this,
replacerCacheKeyFromalways returnscacheEligible = false, andReplaceAllStringFuncruns on every single query — even though the schema value never changes for the lifetime of a River client.The caching mechanism from #802 exists and works correctly, it's just never activated because
Stableisn't set.Impact we observed
We run River in two deployments (an API that enqueues jobs and a worker that processes them). After profiling with
go tool pprofin production:river.Start()with continuous polling):regexp.ReplaceAllStringFuncgrew +29MB in 16 minutes, accounting for 99% of all memory growthregexp.ReplaceAllStringFuncgrew +16.9MB in 20 minutes, accounting for 89% of all memory growthThe strings allocated by the regex are short-lived but under production throughput, the allocation rate outpaces GC and downstream consumers (like pgx tracers or OTel span attributes) can hold references long enough for the memory to accumulate.
We confirmed the fix by patching
schemaTemplateParamlocally via ago.modreplace directive — after deploying withStable: true, memory graphs in our monitoring flatlined.Suggested fix
One-line change in
schemaTemplateParam:The same applies to the
ColumnExistsmethod which also passes a schema replacement withoutStable.The schema value is derived from the client's config and is constant for the lifetime of the River client — it's the textbook case for
Stable: true.This wouldn't affect queries that use dynamic replacements like
where_clauseandorder_by_clause(e.g.,JobList,JobDeleteMany) — those correctly remain uncached since their replacements don't setStable.Environment
Happy to open a PR if you'd like!
Screenshot of our pods in sandbox environment before and after the fix