<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wojtek Pastyrczyk</title>
    <description>The latest articles on DEV Community by Wojtek Pastyrczyk (@wojtekpastyr).</description>
    <link>https://dev.to/wojtekpastyr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4224502%2Fc9e6a3db-db48-4cfb-9155-66837be174f7.png</url>
      <title>DEV Community: Wojtek Pastyrczyk</title>
      <link>https://dev.to/wojtekpastyr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wojtekpastyr"/>
    <language>en</language>
    <item>
      <title>max_wal_size, explained</title>
      <dc:creator>Wojtek Pastyrczyk</dc:creator>
      <pubDate>Tue, 03 Nov 2026 15:34:25 +0000</pubDate>
      <link>https://dev.to/guardioz/maxwalsize-explained-484h</link>
      <guid>https://dev.to/guardioz/maxwalsize-explained-484h</guid>
      <description>&lt;p&gt;&lt;em&gt;max_wal_size gives PostgreSQL's transaction log a size budget. Some write-heavy servers outgrow the 1 GB default - and when that happens, PostgreSQL is forced to write the same data to disk again and again. We explain what the setting really does, how to reduce that wasted disk work using your server's own counters, and when to leave it alone.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting that is easy to overlook
&lt;/h2&gt;

&lt;p&gt;Every busy PostgreSQL server writes more to disk than the application asks it to. Some of that extra work is normal and healthy. But on write-heavy systems, part of it can be repeated, avoidable work - and how much of that happens is heavily influenced by one setting: &lt;code&gt;max_wal_size&lt;/code&gt;. Nothing in daily operation points at it, and the advice that is easy to find online is generic - a fixed number, or a calculator that cannot see your workload. PostgreSQL ships a general-purpose upstream default of 1 GB - the project cannot know how large a server it will run on - and a genuinely busy server can produce that much transaction log in under a minute.&lt;/p&gt;

&lt;p&gt;To see why that matters, you need two concepts. Both are simpler than they sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ideas: the transaction log, and the checkpoint
&lt;/h2&gt;

&lt;p&gt;Before PostgreSQL changes anything in your data files - the files on disk that hold your tables and indexes - it first writes a description of the change to its &lt;strong&gt;transaction log&lt;/strong&gt; (in PostgreSQL language: the WAL, or write-ahead log). The log is the database's insurance against crashes. If the server loses power in the middle of the work, PostgreSQL replays the log on startup and no committed data is lost.&lt;/p&gt;

&lt;p&gt;The log cannot grow forever. So from time to time PostgreSQL saves its progress: it takes all the modified data pages - the 8 KB blocks PostgreSQL stores your tables in - that it holds in memory, and writes them out to the data files. This is a &lt;strong&gt;checkpoint&lt;/strong&gt;. After a checkpoint completes, the log written before it is no longer needed for crash recovery, and PostgreSQL can reuse its disk space - as soon as backups and replication no longer need that part of the log either.&lt;/p&gt;

&lt;p&gt;In normal operation, a checkpoint starts automatically for one of two reasons, whichever comes first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a timer - every 5 minutes by default (&lt;code&gt;checkpoint_timeout&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;volume - the log since the last checkpoint approaches its size budget, 1 GB by default (&lt;code&gt;max_wal_size&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a quiet server the timer always wins, and there is nothing to tune. On a busy one, 1 GB fills quickly - and that is where the real cost begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why frequent checkpoints are so expensive
&lt;/h2&gt;

&lt;p&gt;Here is the less obvious part. After each checkpoint, the first change to any data page is logged in full: with PostgreSQL's default &lt;code&gt;full_page_writes&lt;/code&gt; setting, the entire 8 KB page is copied into the transaction log, not just the few bytes that changed. This is intentional - it protects you from partially written pages if the server crashes during a write.&lt;/p&gt;

&lt;p&gt;Now combine the two mechanisms. Each checkpoint resets that rule: after it, the first change to each page is logged in full again. When checkpoints run every few seconds, frequently changed pages are copied into the log in full over and over. That inflated log reaches its 1 GB budget even sooner, which forces the next checkpoint even earlier. The server ends up writing the same busy pages to disk again and again, and logging more data than the workload truly requires.&lt;/p&gt;

&lt;p&gt;PostgreSQL does warn you: it prints &lt;a href="https://www.postgresql.org/docs/18/wal-configuration.html" rel="noopener noreferrer"&gt;"checkpoints are occurring too frequently"&lt;/a&gt; into the server log. It is easy to miss unless you already watch the server log. And the warning has a blind spot: by default it only appears when checkpoints are less than 30 seconds apart. A checkpoint every 40 seconds is still many times more frequent than the five-minute schedule, but the log stays silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in numbers
&lt;/h2&gt;

&lt;p&gt;We test every Guardioz tuning rule in an internal benchmark environment before we trust it with anyone's database. Here is one of those experiments: the same 30-minute run of pgbench, the benchmark tool that ships with PostgreSQL, using its default scenario - loosely based on TPC-B, with most statements being writes - repeated on PostgreSQL 18 with four different values of &lt;code&gt;max_wal_size&lt;/code&gt;. The experiment compares PostgreSQL's own counters of transaction log volume, data-file write volume, and checkpoints started ahead of the timer - PostgreSQL counts those as "requested" checkpoints. It is not a storage-performance comparison. And because each run completes a different number of transactions, the write volumes are shown per completed transaction, so the settings can be compared fairly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;max_wal_size&lt;/th&gt;
&lt;th&gt;Requested checkpoints&lt;/th&gt;
&lt;th&gt;Transaction log per completed transaction&lt;/th&gt;
&lt;th&gt;Data-file write volume per completed transaction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 GB (the default)&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;14.51 KiB (baseline)&lt;/td&gt;
&lt;td&gt;15.04 KiB (baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;14.24 KiB (-1.8%)&lt;/td&gt;
&lt;td&gt;14.35 KiB (-4.6%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;0 (range: 0-1)&lt;/td&gt;
&lt;td&gt;13.74 KiB (-5.3%)&lt;/td&gt;
&lt;td&gt;13.99 KiB (-6.9%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;13.64 KiB (-6.0%)&lt;/td&gt;
&lt;td&gt;13.97 KiB (-7.1%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Median results from three 30-minute pgbench runs per setting: one 194 GB database (scale 13 300), 64 concurrent clients, PostgreSQL 18, 30 GB of RAM and local SSD storage. For each run, transaction log and data-file write volumes were divided by the number of completed transactions before taking the median. Log volume measured with &lt;code&gt;pg_stat_wal&lt;/code&gt;, data-file write volume with &lt;code&gt;pg_stat_io&lt;/code&gt;, checkpoints with &lt;code&gt;pg_stat_checkpointer&lt;/code&gt;. KiB is 1024 bytes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;max_wal_size&lt;/code&gt; at 1 GB, the server recorded a median of 189 requested checkpoints per 30-minute run - on this isolated benchmark server, all of them demanded by log growth: roughly one every ten seconds instead of one every five minutes. At 32 GB the median fell to zero, with a single requested checkpoint in one of the three runs. And after normalizing for completed work, PostgreSQL generated 5.3% less transaction log and recorded 6.9% less data-file write volume per transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max_wal_size = 1 GB (the default)  ||||||||||||||||||||||||||||||||  about 32 checkpoints
max_wal_size = 32 GB (tuned)       ·······························|  1 checkpoint
                                   0 min                      5 min
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Five-minute illustration derived from the median checkpoint rates above; it is not a trace of an individual run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Raising the setting from 32 GB to 128 GB changed these counters only slightly. Once frequent requested checkpoints had disappeared, a larger limit produced little additional benefit in this experiment, while increasing the worst case for disk usage and recovery time. The goal is enough, not maximum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off
&lt;/h2&gt;

&lt;p&gt;A larger &lt;code&gt;max_wal_size&lt;/code&gt; has a real price. Disk space: PostgreSQL does not reserve the space up front, but under heavy write load the log can genuinely grow to the limit you set - so plan for it. Recovery time: after a crash, PostgreSQL replays the log written since the last checkpoint, so checkpoints spaced further apart can make that restart take longer. For most systems this is an acceptable trade - but make the choice on purpose, not by accident.&lt;/p&gt;

&lt;p&gt;Also know that &lt;code&gt;max_wal_size&lt;/code&gt; is a soft limit. The log can grow beyond it: under heavy load, when transaction log archiving (copying the transaction log to backup storage) is failing, or when the server is told to keep the log around for a replica that has fallen behind or disappeared (a replication slot, or a high &lt;code&gt;wal_keep_size&lt;/code&gt;). Never size it so close to disk capacity that going past the limit could fill the disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tune it yourself
&lt;/h2&gt;

&lt;p&gt;PostgreSQL exposes separate counters for checkpoints scheduled by the timer and checkpoints requested for other reasons - comparing them is where the diagnosis starts. Connect to your database with psql or any other SQL client you normally use, and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;num_timed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats_reset&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_checkpointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(On PostgreSQL 16 and older, the same counters are &lt;code&gt;checkpoints_timed&lt;/code&gt; and &lt;code&gt;checkpoints_req&lt;/code&gt; in &lt;code&gt;pg_stat_bgwriter&lt;/code&gt;.) Two things to know: the counters cover the whole PostgreSQL instance - every database in it, not only the one you are connected to - and they add up from the last statistics reset, whose time the &lt;code&gt;stats_reset&lt;/code&gt; column shows. Take one reading immediately before a representative busy period and another immediately after it, then compare the differences.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nearly all timed?&lt;/strong&gt; Leave the setting alone - even the 1 GB default is correct for that server. A change without a signal has nothing to gain, and something to risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many requested?&lt;/strong&gt; That is often log-size pressure, but not only: manual &lt;code&gt;CHECKPOINT&lt;/code&gt; commands, backups starting, and a few other events are counted here too. Confirm the cause first - with &lt;code&gt;log_checkpoints&lt;/code&gt; enabled (the default since PostgreSQL 15), the server log names it for every checkpoint, and &lt;code&gt;checkpoint starting: wal&lt;/code&gt; means log size. When log size is the cause, raise &lt;code&gt;max_wal_size&lt;/code&gt; and check the counters again. The widely used rule among PostgreSQL experts is to raise it until checkpoints are almost always started by the timer - but never further than your disk can safely absorb. The log must have room to grow to the new limit, and somewhat past it (the limit is soft), and the disk still needs a healthy reserve of free space on top of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counters jump only during a nightly import or a restore?&lt;/strong&gt; Then do not tune the whole server around a single short burst. If that job's speed genuinely matters to you, treat it as its own task; for everything else, tune for how the server works on a normal day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The change itself is cheap: no restart, no downtime. For example, to set 8 GB - again in an SQL session, this time connected as a superuser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;max_wal_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'8GB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_reload_conf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 8 GB here only demonstrates the command - it is not a recommended value. The right number for your server comes from its own transaction log volume, and you confirm it the same way you found the problem: read the counters again over a busy period.&lt;/p&gt;

&lt;p&gt;On managed services it is the same setting in a different place - a parameter group on Amazon RDS, server parameters on Azure Database for PostgreSQL, database flags on Google Cloud SQL - and all three apply it without a restart. Some of them also pick their own starting value - Azure, for example, sizes it from your disk - so check what your server actually runs: open an SQL session, the same way as above, and run &lt;code&gt;SHOW max_wal_size;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  On cloud storage, these writes are on your bill
&lt;/h2&gt;

&lt;p&gt;On your own hardware, avoidable writes use up disk performance you need for real work, and they wear the disks. On cloud storage they can also cost you money, because cloud disks are billed for &lt;strong&gt;provisioned&lt;/strong&gt; performance: you reserve a ceiling of disk operations per second (IOPS), and you pay for that reservation every month whether you use it or not. AWS (gp3, io1, io2), Azure (Premium SSD, Premium SSD v2, Ultra) and Google Cloud all price disk performance this way, in slightly different shapes - included baselines, per-IOPS charges, or IOPS that grow with the disk size you buy.&lt;/p&gt;

&lt;p&gt;A database with frequent checkpoints keeps its disks busier than the workload requires, so the performance ceiling you reserve - and pay for - has to cover wasted writes too. Reduce them, then measure the workload again: when the need has dropped, the same database can fit into a smaller tier, or inside the baseline you already pay for. The saving arrives when you lower what you provision, not by itself. This is not only our view: Microsoft's own &lt;a href="https://learn.microsoft.com/en-us/azure/postgresql/troubleshoot/how-to-high-io-utilization" rel="noopener noreferrer"&gt;troubleshooting guide for high I/O&lt;/a&gt; on Azure PostgreSQL recommends exactly this - raise &lt;code&gt;max_wal_size&lt;/code&gt; until checkpoints are started by the timer.&lt;/p&gt;

&lt;p&gt;One exception worth knowing: Amazon Aurora replaces PostgreSQL's storage layer, so the write mechanics and the tuning procedure described here do not carry over to it directly - what &lt;code&gt;max_wal_size&lt;/code&gt; does there depends on your Aurora engine version, so check its documentation and parameter group. Everything else in this article applies to RDS for PostgreSQL, Azure, Cloud SQL, and any server you run yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  You do not have to do this by hand
&lt;/h2&gt;

&lt;p&gt;Everything above is what Guardioz automates. It starts with measurement: how much transaction log your server produces under its normal, everyday load, and whether checkpoints are being forced ahead of schedule by that log growth. When the measurements say the setting is fine, Guardioz tells you exactly that - no change needed. When there is a real signal, it recommends a &lt;code&gt;max_wal_size&lt;/code&gt; fitted to what your server actually does - not a calculator average - ignores one-off bursts, and first checks that the data disk keeps a safe reserve of free space. The recommendation arrives with the evidence next to it, and applies without a restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your PostgreSQL deserves better.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on the &lt;a href="https://guardioz.com/blog/max-wal-size-explained" rel="noopener noreferrer"&gt;Guardioz blog&lt;/a&gt;. Guardioz makes PostgreSQL tuning end in a decision, not a research project. It is in beta and always free for one user: &lt;a href="https://guardioz.com" rel="noopener noreferrer"&gt;guardioz.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>performance</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
