At a glance
The percentage of the host’s (or instance’s) RAM currently in use by the PostgreSQL workload: shared buffers, per-backend working memory, the operating-system page cache serving the data files, and connection overhead. Memory is what keeps PostgreSQL fast. When there is enough, hot data lives in cache and queries fly; when it runs short, the database spills sorts to disk, the page cache thrashes, and on a self-managed host the OOM killer can terminate the postmaster outright. This gauge is the early-warning line between a database that is comfortably resident in memory and one about to start swapping.
Calculation
The gauge isused_memory / total_memory * 100, sampled in real time. The subtlety in PostgreSQL is what “used” honestly means, because of how the operating-system page cache works.
On a healthy self-managed PostgreSQL host, most of RAM is “used” by the OS page cache holding data-file blocks. That is good: it is the cache that makes reads fast. Linux reports this cache as used memory, but it is reclaimable: the kernel will hand it back to a process that needs it. So a naive “used = total - free” reads near 100% on every healthy database and is useless as an alarm.
The engine therefore tracks the figure that actually predicts trouble:
used = total - available
where available (from MemAvailable in /proc/meminfo) is the kernel’s own estimate of how much memory could be made available to a new allocation without swapping, accounting for reclaimable cache. A gauge built on available stays comfortably mid-range on a healthy database and climbs only when genuine, non-reclaimable demand (backend working memory, anonymous allocations) starts to crowd out the reclaimable cache. That is exactly the condition that precedes spilling and swapping.
PostgreSQL’s own memory has two main consumers the drill-down attributes:
shared_buffersis a fixed shared allocation set at startup (commonly 25% of RAM). It does not grow with load; it is a constant baseline.- Per-backend memory is the variable part. Each connection can use up to
work_memper sort / hash operation (and a single query can run several), plusmaintenance_work_memfor vacuum / index builds. With many connections running memory-heavy queries, this is what drives the gauge up, and it scales with both connection count and query complexity.
FreeableMemory (RDS, conceptually the available figure) or the Cloud SQL usage / quota metrics. The interpretation is the same: the gauge reflects pressure on genuinely allocatable memory, not the benign page cache.
Worked example
A platform team runs a self-managed PostgreSQL 16 primary on a host with 64 GB RAM,shared_buffers set to 16 GB, work_mem set to 64 MB, serving an OLTP order workload plus an analytics read path. Snapshot taken on 28 May 26 at 20:15 BST during an evening reporting peak, after the card paged at 20:08.
The gauge reads 90.6% and has paged. The drill-down makes the cause obvious: per-backend
work_mem is consuming 18 GB, which is wildly out of proportion. Working it through:
- Immediate relief. The analytics read path does not need 64 MB of
work_memper node. Set a lower value just for that workload (per-role or per-session):ALTER ROLE analytics_reader SET work_mem = '16 MB';. This quarters the per-backend appetite for the reporting queries without touching OLTP latency. The gauge falls to 78% within minutes as the next batch of analytics queries runs under the new limit. - Structural fix. The analytics workload should not share the primary’s memory budget with order processing at all. The team plans to route reporting to the read replica, isolating the two so a reporting peak can never again threaten the OLTP primary’s memory.
work_memis a per-operation, per-backend limit, and it multiplies. The headline value looks harmless, but real usage iswork_mem x sort/hash nodes x concurrent backends. Tuning it blindly upward to speed up one query can OOM the host under concurrency. Set it conservatively globally and raise it only for specific roles or sessions that need it.- High page-cache usage is not a problem. A healthy PostgreSQL host shows most RAM “used” by reclaimable page cache, and that is exactly what you want: it is the cache serving fast reads. The gauge is built on available memory precisely so it does not cry wolf at benign cache. Worry when available memory shrinks, not when “free” is low.
- Mixing OLTP and analytics on one instance is a memory-pressure factory. Order processing wants many small fast transactions; analytics wants a few large memory-hungry ones. Sharing one memory budget means the second can starve the first. Route heavy reads to a replica.
Sibling cards
Reconciling against the source
Where to look in PostgreSQL and the host:Host memory truth (self-managed):Why our number may legitimately differ from the native tooling:free -horcat /proc/meminfoshows total, used, free, and cruciallyMemAvailable, the figure the gauge is built on. Watch theSwapline too: any active swapping is already a red flag. Configured allocations:SHOW shared_buffers;,SHOW work_mem;,SHOW maintenance_work_mem;confirm the baseline and per-operation limits. Live backend pressure:SELECT count(*), state FROM pg_stat_activity GROUP BY state;shows how many backends are active and could be holdingwork_mem. Cache effectiveness (the related signal):SELECT sum(blks_hit) * 100.0 / nullif(sum(blks_hit) + sum(blks_read), 0) AS cache_hit_pct FROM pg_stat_database;Managed services: the RDS / Aurora console CloudWatch tab showsFreeableMemoryandSwapUsage; Cloud SQL shows memory usage on the instance overview.
Cross-source reconciliation:
Known limitations / FAQs
My host shows 95% memory used intop but this gauge says 70%. Which is right?
Both, measuring different things. top and naive “used = total - free” count the reclaimable OS page cache as used, which is near-100% on every healthy database because PostgreSQL deliberately fills RAM with cached data blocks. The gauge is built on MemAvailable, the kernel’s estimate of memory that could be allocated without swapping, which is the figure that actually predicts trouble. The gauge is the one to alert on; the top figure is the page cache doing its job.
Why is the alert at 85% and not higher?
Because PostgreSQL performance degrades well before memory is fully exhausted. As available memory shrinks, the page cache shrinks with it, cache-hit rate falls, queries start hitting disk, and sorts spill to temp files, all before you reach 100%. On a self-managed host you also want warning before the OOM killer engages and terminates the postmaster. 85% buys time to act while the database is still healthy.
Raising work_mem made one report faster but the host started OOMing. Why?
Because work_mem is the limit per sort or hash operation, per backend, not per query or per server. A single query can have several memory-using nodes, and dozens of backends can run concurrently. Real peak usage is roughly work_mem x nodes per query x concurrent backends, which at high concurrency dwarfs the host’s RAM. Set work_mem conservatively as the global default and raise it only for specific roles or sessions (ALTER ROLE ... SET work_mem) that genuinely need it.
What should shared_buffers be set to?
A common starting point is 25% of host RAM, with the remainder left for the OS page cache (which PostgreSQL also relies on heavily) and per-backend memory. Pushing shared_buffers much higher often does not help and can hurt, because it steals RAM from the page cache and per-backend memory. It is a fixed allocation set at startup, so it shows as a constant baseline on this gauge, not a variable. Tune it with the Buffer Cache Hit Rate % card, not in isolation.
On RDS / Aurora I cannot get a shell. How is memory measured?
Through the provider metric: RDS and Aurora publish FreeableMemory, which is conceptually the available figure (free plus reclaimable), and the gauge computes used against the instance class RAM. You also get SwapUsage in CloudWatch, which should stay near zero. You cannot run free -h, but the metric is reliable for alerting. If the instance class is undersized for the workload, the answer is to scale up the class or offload reads to a replica.
Memory climbs steadily all day then resets after a restart. Is that a leak?
PostgreSQL itself rarely leaks memory in the classic sense, but a few patterns mimic one: a very high connection count where many backends each hold cached relation metadata, prepared statements accumulating on long-lived connections, or extensions with their own per-backend caches. A gradual climb that resets on restart usually points to long-lived connections accumulating per-backend state; cycling connections through a pooler with a connection lifetime, or using PgBouncer, typically flattens it. Sudden jumps, by contrast, are almost always work_mem under concurrency.
Should I run analytics queries on the same instance as my transactional workload?
Ideally not, especially under memory pressure. OLTP wants many small fast transactions; analytics wants a few large, memory-hungry, long-running queries. Sharing one memory budget means a reporting peak can starve order processing of work_mem and page cache, exactly the scenario in the worked example. Route heavy reads to a read replica so the two workloads have separate memory budgets. This is one of the highest-leverage capacity decisions you can make.