Skip to content

CONCEPT Cited by 1 source

Prepared statement memory accumulation

Definition

Prepared statement memory accumulation is the per-session growth of cached query plans and prepared statements that is not released until the session ends or the statement is explicitly DEALLOCATEd. It is RSS growth driver #5 of 5 that Simeon Griggs names in the 2026-03-30 memory post:

"Cached plans and prepared statements accumulate per- session memory that is not released until the session ends or the statement is explicitly deallocated."

(Source: sources/2026-04-21-planetscale-high-memory-usage-in-postgres-is-good-actually.)

Mechanism

When a client sends PREPARE stmt_name AS SELECT ..., Postgres:

  1. Parses and plans the query.
  2. Stores the parsed query tree + plan in the session's prepared-statement cache.
  3. Returns a handle for subsequent EXECUTE stmt_name(params) calls.

Cached plans live in the backend's memory for the session's lifetime. They are:

  • Non-reclaimable by the kernel (they're in process heap).
  • Not shared across backends (each backend has its own cache).
  • Additive over session lifetime — without explicit DEALLOCATE, they only release on session end.

Why this compounds with long-lived connections

The interaction chain:

long-lived session + many unique query shapes
  → prepared-statement cache grows per session
  → per-backend RSS climbs
  → × active_backend_count
  → cluster RSS rises

Applications that use session-mode pooling (where connections persist for the app's lifetime) accumulate prepared statements indefinitely. Applications that use PgBouncer in transaction mode don't observe this directly because transaction-mode pooling doesn't honour session state — but they can't use prepared statements at all in the traditional sense, a trade-off named in the sibling PgBouncer post's unsupported-features list.

ORM drivers that amplify

Some ORMs auto-create prepared statements for every parameterised query:

  • Rails ActiveRecord historically prepared every query; recent versions allow opting out via prepared_statements: false in database.yml.
  • Python psycopg2 prepares explicitly; psycopg3 has pipeline mode with more aggressive caching.
  • Node.js pg does not prepare by default; pg-native can.

If the ORM auto-prepares and the app churns through thousands of distinct query shapes (dynamic WHERE clauses with many column combinations), the prepared-statement cache grows per backend until memory exhaustion.

Mitigations

  1. Explicit DEALLOCATE of prepared statements the app will not reuse.
  2. plan_cache_mode = force_custom_plan to skip caching entirely (loses plan-reuse benefit).
  3. ORM configuration to disable auto-prepare, or to cap the statement cache at a bounded size.
  4. Transaction-mode pooling via PgBouncer — structurally avoids the problem by rotating server-side connections per transaction.
  5. Periodic backend recycling via idle_session_timeout or connection-lifetime limits.

Seen in

Last updated · 550 distilled / 1,221 read