PostgreSQL query memory planning
PostgreSQL work_mem Calculator
Estimate worst-case PostgreSQL query memory, safe work_mem, spill risk, temp file pressure, and RAM headroom from active query concurrency, sort/hash nodes, shared buffers, maintenance memory, OS reserve, and observed temp file rate.
Worst-case memory breakdown
Recommended setting grid
| Setting | Scope | Typical planning range | Oversizing risk |
|---|---|---|---|
| work_mem | Per sort, hash, materialize, memoize, and similar node | 4 MB to 64 MB for OLTP; 64 MB to 512 MB for scoped analytics | Multiplies by active queries, plan nodes, and sometimes parallel workers. |
| hash_mem_multiplier | Multiplier allowing hash operations to exceed work_mem | 1.0 to 2.0 in many modern PostgreSQL installs | Hash joins and aggregates can consume more memory than a simple work_mem count. |
| maintenance_work_mem | Per maintenance task such as VACUUM, CREATE INDEX, and ALTER TABLE | 256 MB to 2 GB, higher for controlled maintenance windows | Several autovacuum workers or index jobs can allocate it at the same time. |
| shared_buffers | PostgreSQL shared page cache allocated at startup | 20% to 35% of RAM on many dedicated database hosts | Leaves less space for query memory, OS cache, and maintenance work. |
| temp_file_limit | Per session limit for temporary files | Use role-specific limits for reporting and risky ad hoc users | Too low breaks legitimate reports; too high allows disks to fill. |
| log_temp_files | Logging threshold for temp files created by queries | 0 during investigation, then a practical threshold such as 64 MB | No memory risk, but verbose logging can be noisy during heavy spills. |
| Preset | Active queries | Nodes per query | Starting work_mem | Use case |
|---|---|---|---|---|
| Home Lab OLTP | 8 | 1.5 | 8 MB | Small services, simple joins, and light concurrent traffic. |
| Small SaaS API | 24 | 2 | 16 MB | Moderate API workload with index lookups and occasional sorts. |
| Read Heavy API | 48 | 1.8 | 16 MB | Many active reads where memory should remain conservative. |
| Reporting Dashboard | 16 | 4 | 64 MB | Dashboards with grouped reports, ORDER BY, and hash aggregates. |
| Hash Join ETL | 10 | 5 | 128 MB | ETL batches where hash joins and aggregates dominate plans. |
| Parallel Analytics | 8 | 4 | 128 MB | Parallel reports where workers multiply query memory. |
| Timeseries Rollup | 18 | 3 | 48 MB | Continuous aggregation over hot partitions or recent events. |
| GIS Sort Search | 12 | 3.5 | 96 MB | Spatial search and distance sorts with larger intermediate sets. |
| Bulk Index Build | 4 | 2 | 32 MB | Few client queries, but high maintenance memory pressure. |
| PgBouncer OLTP | 60 | 1.4 | 8 MB | High client fan-in where transaction pooling limits backends. |
| Container Postgres | 14 | 2 | 8 MB | Memory-limited Docker or Kubernetes PostgreSQL instances. |
| Temp Spill Incident | 20 | 4.5 | 32 MB | Current workload shows high temp file growth and slow sorts. |
| Signal | Low risk | Watch | High risk | Action |
|---|---|---|---|---|
| Temp file rate | Below 0.5 GB/hour | 0.5 to 5 GB/hour | Above 5 GB/hour | Inspect plans, add indexes, or use targeted work_mem increases. |
| Current vs safe work_mem | Current at or below safe value | Up to 125% of safe value | More than 125% of safe value | Lower global work_mem or scope higher values to reporting roles. |
| Headroom after worst case | Above 15% RAM | 8% to 15% RAM | Below 8% RAM or negative | Reduce concurrency, memory nodes, maintenance overlap, or shared buffers. |
| Parallel worker multiplier | 1 to 1.5 | 1.5 to 3 | Above 3 | Review max_parallel_workers_per_gather and plan shapes. |
| Spill tolerance | Batch/reporting allowed | Occasional spills allowed | Latency-sensitive or no spills | Use lower global values plus SET LOCAL work_mem for known jobs. |
Worst-case memory
The estimate multiplies active queries, memory nodes, current work_mem, and the parallel multiplier. It is a planning envelope, not a guaranteed allocation.
Safe work_mem
The safe value is the available query RAM divided by active memory operations, then adjusted by spill tolerance so global settings do not assume perfect behavior.
Spill risk
Risk rises when current work_mem is above safe work_mem, headroom is tight, temp files grow quickly, or many memory nodes appear in active plans.
Headroom
Headroom is RAM left after shared_buffers, OS reserve, maintenance memory, base overhead, and worst-case query memory are modeled together.
Low global work_mem
Best for high-concurrency OLTP. Keep the global value conservative, then raise memory only for known jobs, roles, or sessions that need it.
Role-specific work_mem
Use ALTER ROLE or ALTER DATABASE for reporting users, ETL users, or admin sessions so ordinary app traffic does not inherit large memory grants.
SET LOCAL for jobs
Batch jobs can set work_mem inside a transaction, run the heavy query, and return to the normal value after commit or rollback.
Query-plan fixes
Indexes, better statistics, lower row estimates, rewritten joins, and partition pruning can reduce the number and size of memory-hungry nodes.
Temp file limits
temp_file_limit can stop runaway spills from filling storage. Pair it with monitoring so failed reports are investigated instead of ignored.
Parallelism control
Parallel workers can make one query allocate memory several times. Tune max_parallel_workers_per_gather for workloads with large sorts or hashes.
If you’ve ever heard of work_mem, you may have assumed that it’s simply a one-off value you enter into postgresql.conf and move on. Nope! It’s a multiplier, and arguably the most dangerous PostgreSQL configuration parameter, since it multiplies by the amount of concurrent activity on your system. Each query gets its own portion of RAM for aggregations, sorts, and hash joins.
Trouble occurs when lots of user is running resource-intensive queries concurrently and the database attempts to consume more RAM than actualy available. Out-of-memory errors result, which in turn stall your applications.
How to Set Work_Mem in PostgreSQL
To do this, just enter your node count and concurrency into calculator above, and it will do the work for you. There’s no need to multiply together multiple variables in your head, which never line up anyway in real world. Most folks freak out when they look at max_connections and realize they have hundreds of connection, so they must reserve lots of memory.
But that ignores the fact that most of those connections aren’t doing anything; and you only want to care about the ones that are actually running at any given time. So instead of asking how much capacity there could be, it asks how many active query are running at same time. It makes you consider reality (what happens on your busiest hour) versus theory (theoretical maximums).
Why does that matter? To understand that, we need to look under the hood at a query plan. For a basic select statement, there may be nothing special about it except what comes out of indexes. Adding something like a group by or order by clause will add some sort or hash nodes to the plan and these nodes claim memory (i.e. Work_mem). If your query has three such node, it could easily triple amount of memory used before completing.
The calculator allows you to account for this by defining how many memory nodes per query you tend to see. You need to look at real execution plans to know whether your reports use two memory nodes or five, which means that experience trumps guessing here as well.
And then you have the operating system reserve. Don’t forget Linux also require memory! Keeping the commonly used pages out of the shared_buffers (via the kernel cache) really helps PostgreSQL. Filling every byte with db settings will starve the OS and hurt performance. By including an OS reserve and some memory for maintenance such as auto vacuum workers, we make sure you’re calculating based off something real world. It leaves headroom so background tasks like this can run and keep the system healthy.
When a node’s available memory runs out, it will write excess to disk in what’s called “spill” files. This kills query speed because disk is slower than RAM. Based on your settings for tolerance and temp file rate, this calculator estimates spill risk. Small spills can hurt performance of latency sensitive API requests; occasional spills can be fine for batch reports as long as it doesn’t crash the server. Knowing which camp you fall into will change how aggressively you tune.
Instead of setting work_mem globally, you should set it as high as needed for certain sessions/roles and lower otherwise. That way, tasks that can do some heavy lifting will have room to breathe, but you avoid having a runaway query using up all the memory. The page also has a useful table showing typical workload types and their recommended settings (ranging from loose to strict). For instance, an ETL pipeline could afford to be more generous with higher values for fewer large jobs than a read-heavy API serving a lot of smaller requests.
Because memory pressure varies with your query workload, and data volume increases over time, tuning is a continuous process. Monitor your temp file log; a spike in disk usage indicates something has gone wrong. Re-check the plans. Were some indexes missed? Are stats out of date? Increasing work_mem may help, but usually this simply hides the real problem. You want to use only what you need while maintaining consistent performance.
This isn’t so much about configuration as budgeting: You only have so much money. Should you place it into a single high-risk investment vehicle? Or should you invest some in each, making sure the lights stay on? Likewise, work_mem is how much you allocate per query. Allocate too little and they’ll crawl, disk spills! Allocate too much and… well, then you don’t really have any left.
That balance is what keeps your server responsive and your users happy. When the database runs out of room, it stops working, and that’s not fun.



