Skip to main content
RunBook Academy

PostgreSQLXI · Memory and Resource ManagementMemory

Temporary files and spilling to disk

Intermediate⏱ ~25 minpsql

What you'll learn

  • Find and quantify spilling on a cluster that is not currently instrumented
  • Use temp_file_limit to bound a query rather than the whole cluster
  • Recognise when an index removes a spill entirely
  • Site and monitor the temporary file location correctly

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

When a node exceeds its memory allowance it writes to disk rather than failing. That is the right behaviour, and it means a cluster can be spilling continuously without anyone knowing.

Finding out whether it is happening

The cumulative counter is on by default and nobody looks at it:

SELECT datname,
       temp_files,
       pg_size_pretty(temp_bytes) AS temp_bytes,
       stats_reset
  FROM pg_stat_database
 WHERE datname IS NOT NULL
 ORDER BY temp_bytes DESC;

As with idx_scan in lesson X-07, the number is meaningless without stats_reset. A hundred gigabytes of temporary files since the last restart is a very different statement depending on whether that was yesterday or last year.

Then turn on the per-event logging, which is off by default:

ALTER SYSTEM SET log_temp_files = 0;      -- log every temporary file
SELECT pg_reload_conf();
Read-only / Safewhat a spill looks like in the log
$ tail -f /var/log/postgresql/postgresql.log
LOG:  temporary file: path "base/pgsql_tmp/pgsql_tmp2684.0", size 52117504

The default of -1 means never. 0 means all. A size threshold such as '10MB' is the sensible compromise on a busy cluster: it records the spills that matter and ignores the trivial ones.

Pair it with log_min_duration_statement so the spilling statement is identifiable, and with a log_line_prefix including %a so it can be attributed to a service.

Bounding it

temp_file_limit caps how much temporary file space a single session may use. Exceeding it raises an error rather than filling the disk.

Service impact possibletemp_file_limit against a sort needing about 51 MB
$ psql -U postgres -c "SET temp_file_limit='8MB'; SELECT count(*) FROM (SELECT * FROM ordersx ORDER BY amount, created_at) s"
SET
ERROR:  temporary file size exceeds "temp_file_limit" (8192kB)
Read-only / Safethe same query with the limit removed
$ psql -U postgres -c "SET temp_file_limit='-1'; SELECT count(*) FROM (SELECT * FROM ordersx ORDER BY amount, created_at) s"
  count
---------
2000000

The better answer: remove the spill

Read-only / Safethe same sort, with an index on the leading sort column
$ psql -U postgres -c "SET temp_file_limit='8MB'; EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) SELECT count(*) FROM (SELECT * FROM ordersx ORDER BY amount, created_at) s"
 Full-sort Groups: 1000  Sort Method: quicksort  Average Memory: 27kB  Peak Memory: 27kB
Pre-sorted Groups: 1000  Sort Method: quicksort  Average Memory: 111kB  Peak Memory: 111kB

Compare the three versions of the same query:

ApproachMemoryDisk
Default work_mem, no index50,896 kB spilled
work_mem = 1GB, no index111,653 kBnone
Index on (amount), any work_mem111 kB peaknone

An index on the leading sort column let PostgreSQL read rows in approximate order and sort them in small groups — an incremental sort — so it never accumulated the whole result. Peak memory fell by three orders of magnitude, and it succeeded even under the 8 MB limit that had just rejected the same query.

This is the answer to look for first. Raising work_mem accepts the work and buys memory for it. An index avoids the work.

The shapes where it applies:

  • ORDER BY on an indexed column, or a leading prefix of one.
  • GROUP BY on an indexed column, which can use a GroupAggregate over ordered input instead of a hash table.
  • A merge join where both inputs are already ordered by their indexes.

Where the files go

Temporary files live in pgsql_tmp inside each tablespace’s directory, or in base/pgsql_tmp for the default tablespace.

-- put them somewhere with room, and separate failure domains
CREATE TABLESPACE tempspace LOCATION '/mnt/fasttemp';
ALTER SYSTEM SET temp_tablespaces = 'tempspace';
SELECT pg_reload_conf();

Two reasons this is worth doing on a busy cluster:

Failure isolation. Temporary files filling the data directory stops the cluster. Filling a separate filesystem stops the queries that were spilling.

Independent sizing and monitoring. Temporary space is bursty and unrelated to data growth. Watching one number that combines them tells you nothing.

What to take from this

  • pg_stat_database.temp_files and temp_bytes are already being collected. Read them with stats_reset.
  • log_temp_files defaults to never. Set it to 0 or a size threshold.
  • temp_file_limit is per session and superuser context, so it is a real guard. It does not bound the cluster.
  • Measured: an index on the sort column reduced peak memory from 111,653 kB to 111 kB and removed the spill. Look for that first.
  • Site temporary files on their own filesystem for failure isolation.
  • Orphaned temporary files clear on restart. Do not delete them by hand on a running server.

Cross-course references

  • Linux for Production Sysadmins — Part XIV (Filesystems) covers the filesystem that fills when a query spills, and Part XLI (Storage Performance) covers whether spilling is expensive on your device or merely different.
  • Observability for Production Sysadmins — Part LIX (Database observability) covers exporting temp_bytes as a rate, which is what distinguishes a query that always spills from one that started to.

Quiz

Knowledge check · 6 questions

  1. Q1. A query spills 51 MB sorting by an indexed column. Which change reduces its memory requirement the most?

  2. Q2. A cluster sets temp_file_limit to 2 GB per session, yet a single query wrote 6 GB of temporary files. How?

  3. Q3. The pgsql_tmp directory on a long-running cluster contains files with old timestamps and pids that no longer exist. What is the correct action?

  4. Q4. Which query shapes can avoid a spill entirely by using an existing index? Select all that apply.

  5. Q5. Setting temp_file_limit per role is an effective guard because it is superuser context, so an ordinary user cannot raise their own limit.

  6. Q6. You inherit a cluster with no temporary file instrumentation. Describe how you would find out whether spilling is a problem.

Passing score: 75%. Answers are checked in this browser.