RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
PostgreSQL Memory Context Exhaustion Tool
02. Root Cause Vector

PostgreSQL memory context exhaustion kills backend processes when a single allocation request exceeds available context memory, triggerin...

03. Rapid Prescription

Enforce consistent lock order (e.g. ORDER BY id FOR UPDATE) or use non-blocking SKIP LOCKED.

Zero-Trust Client IDE/ Performance

PostgreSQL Memory Context Exhaustion Tool

PostgreSQL memory context exhaustion kills backend processes when a single allocation request exceeds available context memory, triggering immediate query failure and potential connection storm.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(66L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# WARNING: This is a legacy deployment script for the PCI-secure-zone. DO NOT MODIFY unless you know the 2008-era conventions.
# TODO: Refactor this memory-hungry batch processing; it's causing sporadic OOMs in prod but the vendor API expects this format.
# FIXME: The temp table bloat issue is still unresolved; we just increased shared_buffers as a workaround.

import psycopg2
import os
from datetime import datetime
import sys

# Legacy connection string from the old vault migration (keep for fallback)
db_uri = 'postgresql://app_user:SuperSecretLegacyPass123!@pci-db.internal:5432/transactions?sslmode=require'
# Newer env var (sometimes set by the orchestrator, but not always)
DB_CONN = os.getenv('DB_SECURE_URI', db_uri)

# Dummy AWS keys for the deprecated logging service (still referenced in some cron jobs)
AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE'
AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'

# Stripe test key left from the payment integration sandbox (should be removed)
STRIPE_API_KEY = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'

# Multi-line RSA private key for SSH tunneling (old backup method)
SSH_PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArX8zW5N5Y6JwXaNvB7z8QkHq1KjTpLmM0oWqY7GvH8sF
... (truncated for brevity) ...
-----END RSA PRIVATE KEY-----'''

def process_bulk_transactions():
    """
    Batch processes transaction logs from the legacy system.
    This is known to cause memory spikes but the business insists on real-time aggregation.
    """
    conn = None
    try:
        conn = psycopg2.connect(DB_CONN)
        cursor = conn.cursor()
        # VULNERABILITY: Unbounded recursive CTE with large dataset in a single transaction, exhausting memory context.
        # The recursive query aggregates all historical data each run, but the WHERE clause is misapplied due to a typo in the column name.
        cursor.execute("""
            WITH RECURSIVE audit_chain AS (
                SELECT id, parent_id, amount, created_at 
                FROM pci_transactions 
                WHERE created_at > '2023-01-01'  -- Meant to limit, but parent_id recursion ignores this
                UNION ALL
                SELECT t.id, t.parent_id, t.amount, t.created_at
                FROM pci_transactions t
                INNER JOIN audit_chain ac ON t.parent_id = ac.id
                -- Missing termination condition; will recurse through entire hierarchy
            )
            SELECT sum(amount) FROM audit_chain;  -- This accumulates all rows recursively in memory
        """)
        total = cursor.fetchone()[0]
        print(f"Total aggregated: {total}")
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        # TODO: Add proper alerting to Slack channel #legacy-alarms
    finally:
        if conn:
            conn.close()

if __name__ == "__main__":
    # This is called by a cron job every 5 minutes; memory leaks accumulate.
    process_bulk_transactions()
    # Old debug line left from the 2019 outage investigation
    # print("DEBUG: Memory usage peak:", resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix PostgreSQL 'Out of Memory: Failed on Request of Size X' in Memory Context

Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–45 mins

TL;DR

  • What broke: A PostgreSQL backend process attempted to allocate a chunk larger than its assigned memory context ceiling — query dies with ERROR: out of memory, and under connection pooling this cascades into a failure storm.
  • How to fix it: Tune work_mem, max_stack_depth, and rewrite the offending query to avoid unbounded sort/hash operations or leaking PL/pgSQL SPI contexts.
  • Use the Client-Side Sandbox above to paste your postgresql.conf and failing query — it auto-generates the refactored config and rewritten SQL without sending your data anywhere.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw error from PostgreSQL logs:

ERROR:  out of memory
DETAIL:  Failed on request of size 1073741824 in memory context "ExecutorState".

or in PL/pgSQL:

ERROR:  out of memory
DETAIL:  Failed on request of size 524288000 in memory context "SPI Proc".

PostgreSQL memory contexts are per-backend, hierarchical allocators. Each query spawns child contexts (ExecutorState, SortState, HashBatch, SPI Proc, etc.) under the top-level TopMemoryContext. When a single palloc() call inside a context cannot be satisfied — either because the OS refused the allocation or the context's own ceiling is hit — Postgres throws this error and aborts the transaction immediately. The backend process itself survives, but the query is dead. Under PgBouncer or high-concurrency workloads, dozens of backends hitting this simultaneously will saturate your connection pool and cause a cascading outage.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a soft degradation. The failure chain is:

  1. Single large query (unbounded sort, hash join on a fat table, IN (...) with 500k values, or a PL/pgSQL loop that never frees SPI context) requests a multi-hundred-MB palloc inside ExecutorState.
  2. OS-level OOM (vm.overcommit_memory behavior) either refuses the allocation outright or the kernel OOM killer fires and shoots the postmaster — taking down all active connections on that instance.
  3. Connection pool exhaustion: PgBouncer retries dead connections; application threads pile up waiting; upstream services time out. A single bad query on a shared RDS or Cloud SQL instance can take down unrelated microservices sharing the same Postgres endpoint.
  4. Replication lag spike: If the primary is OOM-killed, standby promotion takes 30–120 seconds. Any writes during that window are lost if synchronous_commit is off.

The highest-risk patterns that trigger this:

  • work_mem set globally too high × high max_connections (e.g., work_mem=512MB × 200 connections = 100 GB theoretical peak)
  • SELECT * FROM large_table ORDER BY unindexed_col — full sort in ExecutorState
  • Recursive CTEs without LIMIT or cycle detection
  • PL/pgSQL functions with PERFORM loops that accumulate SPI sub-contexts without COMMIT or explicit context resets
  • array_agg() or string_agg() over millions of rows

How to Fix It (The Solution)

Basic Fix — Tune `work_mem` and Add a Query Safeguard

The most common root cause is work_mem being set too high globally or a query needing more than available. The fix is to scope work_mem per-session for heavy queries and add a statement-level memory guard.

# postgresql.conf

- work_mem = 512MB          # Catastrophic at high concurrency
+ work_mem = 16MB           # Safe global default

- max_connections = 200
+ max_connections = 100     # Reduce peak memory ceiling

+ # Add statement timeout as a circuit breaker
+ statement_timeout = '120s'

For a specific heavy analytical query, override per-session instead of globally:

- SET work_mem = '1GB';  -- set globally in app connection string
- SELECT * FROM orders ORDER BY created_at;

+ -- Scope only to this transaction
+ BEGIN;
+ SET LOCAL work_mem = '256MB';
+ SELECT * FROM orders ORDER BY created_at LIMIT 10000;
+ COMMIT;

Enterprise Best Practice — PL/pgSQL Context Leak Fix + Connection-Level Guardrails

If the context is SPI Proc, you have a leaking PL/pgSQL function. The fix is explicit subtransaction management and chunked processing:

-- Leaking function pattern
CREATE OR REPLACE FUNCTION process_all_events() RETURNS void AS $$
DECLARE
  rec RECORD;
BEGIN
- FOR rec IN SELECT * FROM events LOOP   -- loads all rows into SPI context
-   PERFORM heavy_transform(rec);
- END LOOP;
END;
$$ LANGUAGE plpgsql;

+ -- Fixed: chunk with LIMIT/OFFSET and commit per batch to release SPI context
CREATE OR REPLACE FUNCTION process_all_events() RETURNS void AS $$
DECLARE
+  batch_size INT := 1000;
+  offset_val INT := 0;
+  rows_processed INT;
BEGIN
+  LOOP
+    WITH batch AS (
+      SELECT * FROM events ORDER BY id LIMIT batch_size OFFSET offset_val
+    )
+    SELECT COUNT(*) INTO rows_processed FROM (
+      SELECT heavy_transform(rec) FROM batch
+    ) t;
+    EXIT WHEN rows_processed < batch_size;
+    offset_val := offset_val + batch_size;
+    PERFORM pg_sleep(0);  -- yield, allows context GC
+  END LOOP;
END;
$$ LANGUAGE plpgsql;

For RDS/Cloud SQL, enforce memory guardrails at the parameter group level:

# RDS Parameter Group (or Aurora cluster parameters)

- work_mem = 0              # 0 = use engine default (dangerously high on large instances)
+ work_mem = 16384          # 16MB in kB
+ temp_file_limit = 10485760  # 10GB max temp spill per session — prevents unbounded disk use too
+ huge_pages = try           # Reduce TLB pressure on large work_mem workloads

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Enforce work_mem caps in Terraform/Pulumi IaC:

# terraform/rds_parameter_group.tf
resource "aws_db_parameter_group" "postgres" {
  family = "postgres15"

- # No work_mem override — inherits instance default

+ parameter {
+   name  = "work_mem"
+   value = "16384"   # 16MB — enforce in code review
+ }
+ parameter {
+   name  = "temp_file_limit"
+   value = "10485760"
+ }
}

2. Checkov policy to block high work_mem in IaC PRs:

# checkov custom check: check_work_mem.py
from checkov.common.models.enums import CheckResult
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class WorkMemCheck(BaseResourceCheck):
    def __init__(self):
        super().__init__("Ensure work_mem <= 64MB", "CKV_CUSTOM_PG_001",
                         ["aws_db_parameter_group"])

    def scan_resource_conf(self, conf):
        for param in conf.get("parameter", []):
            if param.get("name") == ["work_mem"]:
                if int(param["value"][0]) > 65536:  # 64MB in kB
                    return CheckResult.FAILED
        return CheckResult.PASSED

3. pg_stat_statements monitoring alert (Datadog / Prometheus):

-- Identify top memory-pressure queries before they OOM
SELECT query, calls, mean_exec_time, stddev_exec_time,
       rows / calls AS avg_rows
FROM pg_stat_statements
WHERE rows / NULLIF(calls, 0) > 100000  -- queries returning 100k+ rows avg
ORDER BY mean_exec_time DESC
LIMIT 20;

Wire this query into your monitoring pipeline as a scheduled job. Alert when avg_rows > 50000 on any single statement — that's your early warning before the OOM fires in production.

4. Load test gate in CI: Run pgbench or a custom k6 Postgres script against a staging clone with production-scale data before merging schema changes or new analytical queries. Fail the pipeline if any query allocates > 256MB per EXPLAIN (ANALYZE, BUFFERS).

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.What is a PostgreSQL memory context and why does it exhaust?
PostgreSQL uses a hierarchical palloc() allocator divided into named contexts (ExecutorState, SortState, SPI Proc, etc.). Each backend process owns these contexts for the lifetime of a query or function call. Exhaustion is a critical failure that occurs when a single allocation request—typically from an unbounded sort, hash join, large IN() list, or a PL/pgSQL loop that accumulates SPI sub-contexts without releasing them—exceeds what the OS will grant or what the context ceiling allows. This is a deep architectural flaw in your query's resource management, and the backend aborts the transaction immediately, causing service disruption. Understanding this root cause is the first step to eliminating the vulnerability.
Q2.How do I find which query is causing the OOM without reproducing the crash?
Manually hunting for the offending query is a high-risk, error-prone trap. While you can enable `log_min_messages = warning` and `log_temp_files = 0` or query `pg_stat_statements` for high `rows/calls` ratios, these are reactive, noisy, and often miss the root cause in complex deployments. Manual analysis introduces a significant security vulnerability: you risk exposing sensitive query patterns and data structures in logs, and a misdiagnosis can lead to a wider blast radius. Do not attempt manual remediation. Instead, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that isolates and patches the leak without exposing your core logic.
Q3.Is setting work_mem higher always the cause? Can I just increase RAM?
Throwing more RAM at the problem is a temporary, costly, and ultimately futile enterprise-scale mistake. work_mem is allocated *per sort operation per query*, not per connection—a single query with four sort nodes uses 4× work_mem. On a large instance, the theoretical peak can exceed physical memory by hundreds of gigabytes. The correct fix requires rewriting queries to use indexes or LIMIT clauses, but doing this manually across large mono-repos or at scale is a compliance and consistency nightmare. It creates audit drift and unpatched vulnerabilities in legacy code. Upgrade to the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review that enforces memory guardrails and generates compliant, optimized patches across your entire codebase before deployment.