Reproduces a memory retention issue in PgDog's global prepared statements cache
(observed on v0.1.50, mechanism present on main).
Long-lived clients that keep preparing unique SQL statements (report jobs,
ORMs interpolating values into SQL, cron batches) grow the global cache
(frontend/prepared_statements/global_cache.rs) to millions of entries. Entries
are only freed after the client closes the statement or disconnects — that part
works. The problem:
HashMap/HashSetnever return capacity to the allocator. After a spike of N entries the three tables (statements,names,unused) keep ~2N slots forever: for 1M statements that is ~0.8 GiB of empty buckets at 100 live entries, and it scales linearly from there.- The
prepared_statements_memory_usedmetric sums live entries only (MemoryUsage for HashMapiterates), so it reports ~0 while RSS holds gigabytes.
Requires docker compose and python3 with psycopg[binary].
docker compose up -d
pip install 'psycopg[binary]'
# 1M unique statements, ~7 minutes at ~2.5k statements/s
python3 loadgen.py --host 127.0.0.1 --port 6432 --conns 16 \
--total-qps 100000 --unique-fraction 1.0 --hold-seconds 60Watch during and after the run:
watch -n 5 'curl -s localhost:9090/metrics | grep -E "^prepared_statements"; \
docker stats pgdog --no-stream --format "RSS {{.MemUsage}}"'| phase | prepared_statements | memory_used | container RSS |
|---|---|---|---|
| load peak | 1 000 015 | 416 MiB | 1 725 MiB |
| clients disconnected | 100 | 43 KiB | 1 462 MiB |
| 10 minutes later, stable | 100 | 43 KiB | 971 MiB |
The maintenance sweep correctly trims entries to prepared_statements_limit,
but ~1 GiB of RSS never recovers (a fresh instance idles at ~25 MiB), and the
metric claims the cache is empty. The retention scales with the spike size:
the same run with 4.7M unique statements leaves a 4.8 GiB plateau.
- The
loadgen.pyclient mimics drivers that never sendClosefor prepared statements while the connection lives (psycopgwithprepared_maxeffectively unbounded).