A client that opens many distinct server-side prepared statements over the extended protocol and keeps them open makes PgDog's prepared-statement cache grow to gigabytes.
Key points:
prepared_statements_limitdoes not bound this. It only controls eviction of unused statements (per-server-connection LRU, which protects the backend Postgres). In-use statements (still referenced by a connected client,used > 0) are never evicted, so the pgdog-side cache grows without bound.SHOW MEMORYmassively undercounts it (a few MB reported while RSS is multiple GB) — it counts entry strings, not the cache'sHashMapcapacity /Statement(parse + RowDescription) sizes.- After the client disconnects, memory is only partially released (~1 GB stays).
- The backend Postgres stays flat — the growth is entirely PgDog-side.
- Independent of
query_parser(onandoffboth balloon) and ofquery_parser_engine(protobuf/raw) — the growth is in the extended-protocol prepared-statement handling (PreparedStatements::insert→GlobalCache), not the query parser.
Root cause (source): frontend/prepared_statements/global_cache.rs. GlobalCache holds
statements: HashMap<CacheKey, CachedStmt> and names: HashMap<String, Statement>. Each distinct
prepared statement adds an entry with a reference count used. close_unused(limit) only evicts
entries with used == 0. While a client keeps statements open, used >= 1, so nothing is evicted
and both HashMaps grow unbounded (plus the per-client local: HashMap<String,String>).
Tested on ghcr.io/pgdogdev/pgdog:0.1.48.
docker compose up -d
pip install "psycopg[binary]"
python3 repro.py 500000Watch PgDog's RSS climb into the GBs while repro.py runs (docker stats), and compare with what
PgDog reports:
PGPASSWORD=adminpass psql -h 127.0.0.1 -p 6432 -U admin -d admin \
-c "SHOW CLIENT MEMORY" -c "SHOW SERVER MEMORY" \
-c "SELECT count(*) FROM (SHOW PREPARED STATEMENTS) t"| config | PgDog RSS | SHOW PREPARED STATEMENTS | notes |
|---|---|---|---|
| baseline | 27 MB | — | |
query_parser=on, 625k prepared open |
2850 MB | 625646 | SHOW MEMORY totals 49 MB (~50x undercount); Postgres flat 78 MB |
query_parser=off, 500k prepared open |
2389 MB | 500000 | same behaviour — not parser-related |
| after client disconnect | ~1039 MB | per-client map freed (~1.8 GB), but ~1 GB not released |
prepared_statements_limit = 100 in all cases — it does not cap in-use statements.