Errata for the cpu_index_tuple_cost, cpu_operator_cost, and cpu_tuple_cost post (2026-06-01): that post says the 4.0 default assumes a spinning disk on which a seek-and-read runs about four sequential reads. That is the folk explanation, and it is wrong. On a spinning disk a random read runs more like forty sequential reads; the documentation explained 4.0 as that ratio discounted by an assumed 90% cache hit rate, for thirteen years, until the explanation was removed in October 2025. The details are below.

random_page_cost has never been the cost of a random page read. It is the cost of a random page read multiplied by the chance that the read reaches the disk at all, and nearly everything people get wrong about it comes from reading the name instead of the definition.

Both parameters are user context, floating point, with a minimum of 0 and no maximum anyone will ever hit. seq_page_cost defaults to 1.0 and is the unit the entire cost scale is built on; cpu_tuple_cost is 0.01 because a row’s worth of CPU is taken to be a hundredth of a sequential page fetch, and so on down through the CPU cost constants. random_page_cost defaults to 4.0, and has since it became a parameter in 7.0 in 2000. seq_page_cost is younger: until 8.2 the sequential fetch was an implicit constant you could not change. Since 9.0 both can be overridden per tablespace with ALTER TABLESPACE ... SET, which is the only way to tell the planner that two kinds of storage live under one cluster.

What is charged at which rate

The planner charges seq_page_cost for every page of a sequential scan, so a 34,483-page table I built for this post costs exactly 34,483 + 1,999,998 × 0.01 = 54,482.98 to read in full, and EXPLAIN prints that number to the penny. It charges random_page_cost for each index page a scan is expected to read (the upper levels of a B-tree are charged as CPU rather than as pages, on the theory that they stay cached), for each heap page a bitmap heap scan fetches when the bitmap is sparse (sliding toward seq_page_cost as the bitmap fills, by the square root of the fraction of the table being fetched), for every tuple of a TID scan, and for a quarter of the page traffic of an external sort, the other three quarters being charged sequential. A hash aggregate that spills writes at the random rate and reads back at the sequential one; hash join batches are sequential throughout. The temp-file charges always use the global values. A tablespace override never reaches them, and I confirmed that by dropping pg_default’s costs to 0.01 and watching the Seq Scan under a spilling Sort get cheaper while the Sort node’s own charge did not move.

The interesting charge is the heap side of a plain index scan, because that is where random_page_cost meets pg_stats.correlation. For an index whose order has nothing to do with the table’s physical order, the planner estimates the number of distinct heap pages the scan will touch with the Mackert-Lohman formula (effective_cache_size is the cache size that formula takes) and charges each of them random_page_cost. For a perfectly correlated index it charges one random fetch and then seq_page_cost for every page after it. In between it interpolates linearly on the square of the correlation. Here is a 2,000,000-row table with an index on a column whose correlation is 1 and another on a column whose correlation is -0.002, with bitmap scans turned off so the plain index scan has to show its price:

1=# SET enable_bitmapscan = off;
2=# EXPLAIN SELECT * FROM t WHERE r < 5000; -- uncorrelated index
3 Index Scan using t_r on t (cost=0.43..32709.26 rows=9200 width=109)
4=# EXPLAIN SELECT * FROM t WHERE id < 9200; -- correlated index
5 Index Scan using t_id on t (cost=0.43..392.78 rows=8420 width=109)

About the same number of rows, the same random_page_cost, an eighty-fold difference in cost. The uncorrelated scan is 8,118 estimated heap pages at 4.0 apiece, 32,472 of the 32,709; the correlated one is 146 heap pages, one at 4.0 and 145 at 1.0. Set random_page_cost = 1.1 and the first drops to 9,112 while the second drops to 320. That is the lever that decides plans: random_page_cost changes the price of index scans in proportion to how uncorrelated the index is, and everything else it touches moves at the margins. It is also why a freshly CLUSTERed and ANALYZEd table gets index plans the same table did not get the day before, and why those plans go away again as updates erode the correlation.

Where 4.0 came from, and where it is going

Tom Lane’s planner rewrite in 7.0 set DEFAULT_RANDOM_PAGE_COST to 4.0, on spinning disks, at a time when a random read cost something like forty sequential ones. The 7.0 source defines a sequential fetch as costing 1.0 and explains the gap only by noting that kernel read-ahead makes sequential reads cheaper than the raw disk would; four rather than forty was never defended as a device benchmark. By 8.2 the folk advice had already drifted well below four for OLTP, enough that the 8.2 release notes, announcing a fix to nested-loop index scan costing, suggested that unrealistically small values of the parameter might no longer be needed; a good deal of what people believed about it was a workaround for the planner, not an observation about disks. The explanation that stuck arrived in 2012, when Bruce Momjian wrote into the 9.2 documentation that the default models random access as forty times slower than sequential while “expecting 90% of random reads to be cached.” That remains the right way to read it: a hardware ratio multiplied by a cache miss rate, neither of which the documentation has ever had a way of telling you for your system.

That sentence left the docs on October 30, 2025, in a rewrite by Bruce Momjian backpatched to 13, and it took with it the suggestion, added in 2020 and backpatched to 9.5, that solid-state storage might be modeled with 1.1. The replacement text keeps caching, adds the latency of network-attached storage as a second reason the value is lower than the hardware alone would suggest, and recommends no number for SSDs at all. The patch was posted into a pgsql-hackers thread Tomas Vondra had started that month, in which he measured the ratio the direct way: a 182GB uniformly random table on a 64GB machine, time a full sequential scan, time a full index scan, divide per page. He got about 20 on a single NVMe drive, 21 on a four-drive SATA SSD stripe, 49 on a four-drive NVMe stripe, and 63 from a smaller direct-I/O run on an Azure standard HDD volume. His conclusion was that the default is too low rather than too high, flash included, and that something near 20 would mis-plan less often. Andres Freund objected that the method charges the index scan’s much larger per-row CPU work to the page cost; his own pg_prewarm comparison of random against sequential reads came out at almost exactly four to one. Nobody changed the default, and it is still 4.0 in 19 beta 4. What nobody in the thread disputed is the mechanism: a plain index scan reads heap pages one synchronous request at a time, with no prefetching (the PostgreSQL 18 AIO work streams reads for sequential scans and bitmap heap scans; through 19 beta 4 an ordinary index scan still waits for each heap page before asking for the next), so a heap fetch that misses cache pays the device’s full single-request latency, and the only question is how often that happens.

Which is a question you can answer. pg_stat_io (16 and later) reports hits against reads for client backends; pg_statio_user_tables has heap_blks_hit against heap_blks_read per table; and EXPLAIN (ANALYZE) on the queries you are fighting shows shared hit against read on the index scan node, index and heap pages together (18 turns BUFFERS on by default; add it yourself on anything older). If the heap pages reached through indexes are overwhelmingly hits, which is what an OLTP system with its working set in memory looks like, the raw hardware ratio does not matter and 1.1 is correct. The gap between 1.1 and 1.0 is small enough that it will rarely decide a plan by itself; 1.1 survives because even on flash a sequential read gets read-ahead and I/O combining and a random one does not, and because the model otherwise has no tiebreaker. PGX sets 1.1 on SSD-backed OLTP systems as a matter of routine, and has for years, and I cannot point to a case where it made a plan worse. If the database is several times larger than memory and the queries are analytical, leave it at 4.0, understand that Vondra’s numbers say it is low rather than high, and treat the planner’s preference for sequential scans in that environment as the feature it is.

The ways it gets set without you noticing

A tablespace override does not show up in SHOW. After ALTER TABLESPACE pg_default SET (random_page_cost = 1.1), SHOW random_page_cost still says 4 and every plan on the cluster is costed at 1.1; the only places the override is visible are pg_tablespace.spcoptions and \db+. Check there before concluding that a server is on the defaults. The override is also per relation rather than per query: an index on a fast tablespace and its table on a slow one are charged at two different rates within the same scan, which is what you want when the index fits on the NVMe and the heap does not.

seq_page_cost is the unit, and you should leave it at 1.0 unless you mean to change what a unit is. The one legitimate reason is a database that is entirely in memory, where the documentation suggests lowering both page costs so that CPU dominates. Before you do that, remember that parallel_setup_cost, parallel_tuple_cost and the jit_*_cost thresholds are absolute numbers on the same scale, so cutting the page costs by ten also means a query needs ten times as much estimated I/O before JIT engages (where JIT is on; 19 turns it off by default), and raising the CPU constants instead pushes the other way and makes parallel plans look cheaper than they are, because CPU is the part of a scan’s cost the planner divides among workers. There is no clean version of this change; make it with a benchmark or not at all.

So: random_page_cost = 1.1 for an OLTP system whose working set fits in memory on solid-state storage, 4.0 for anything that does not fit, per tablespace when you have both kinds under one cluster, and seq_page_cost at 1.0, because it is the ruler and not the measurement.