Infrastructure engineering for East Africa's operators, platforms and regulators See the work →
PostgreSQL Point-in-Time Recovery: What 35 Days Costs
Linux 7 min read

PostgreSQL Point-in-Time Recovery: What 35 Days Costs

PostgreSQL point-in-time recovery cost, measured: a 755 MB database at 100 transactions a second wrote WAL at 80 GB a day. What RDS, Azure and S3 bill.

CE
CloudSpinx Engineering
Infrastructure Engineering, Virtualization, Networking, Automation
29 October 2026
postgresqlpoint-in-time recoverywal archiverds backup storage

Retention is the number people argue about when they set up PostgreSQL point-in-time recovery. Outside Amazon RDS, which by AWS's account does not count it, the bill is set by a second number: how much WAL the database writes in a day. On our test server a 755 MB database taking 100 small transactions a second wrote 1.67 GB of it in half an hour. Kept for 35 days, the longest window Amazon RDS and Azure Database for PostgreSQL offer, that comes to 2,790 GB of archived WAL files behind less than 1 GB of data.

CloudSpinx runs PostgreSQL for organizations in Kenya, on servers we operate or in the client's own cloud account, and a managed PostgreSQL quote has to say how far back the database restores. So we measured what that window weighs: PostgreSQL 18.6 with its default WAL and checkpoint settings, pgbench at a fixed 100 transactions a second, one change per run unless noted. Then we priced the result the three ways it gets billed: RDS in Cape Town, Azure's South Africa North region, and an archive you keep yourself on S3.

How much WAL does a PostgreSQL database write in a day?

About 100 times its own size, in our run: 80 GB a day from a 755 MB database. Very little of it is the changes themselves. We ran pg_waldump --stats over the 100 segment files the run archived, and full-page images were 94.7% of the bytes. The full_page_writes documentation explains why: the first time a page changes after a checkpoint, PostgreSQL logs the entire page, and checkpoints come every five minutes by default. Each one restarts the count.

Where the WAL comes from: the minutes after each checkpoint
Where the WAL comes from: the minutes after each checkpointWAL written per 30 seconds over 30 minutes by the same 755 MB PostgreSQL 18 database at 100 transactions a second. With the default 5 minute checkpoint interval the rate climbs back to between 30 and 46 MB per 30 seconds after each checkpoint and falls to about 22 to 25 before the next, for 1.67 GB in half an hour. With a 15 minute interval and max_wal_size raised to 4GB it falls to about 10 MB before the one checkpoint at 15 minutes, for 1.16 GB.1020304050MB of WAL per 30 sthe 15 min run checkpoints once, here0 min5 min10 min15 min20 min25 min30 min1.67 GB1.16 GBDashed verticals: timed checkpoints at the 5 minute interval
  • checkpoint_timeout 5min (default)
  • checkpoint_timeout 15min, max_wal_size 4GB
Same database, same load, checkpoint interval changed: checkpoints every 5 minutes wrote 1.67 GB of WAL in half an hour, every 15 minutes (max_wal_size raised to 4GB) 1.16 GB.

The curve resets because of full-page writes. The first change to a page after a checkpoint logs an image of the whole page, and those images were 94.7% of the WAL bytes in the 5 minute run.

PostgreSQL 18 changed a default that adds to this. Its initdb now enables data checksums by default, the same default behind the checksum refusal on a PostgreSQL 14 upgrade, and with checksums on, the docs say hint bit updates are always WAL-logged, as an image of the whole page when the page has not been logged since the last checkpoint. Those page images show up in pg_waldump as FPI_FOR_HINT, and they were 36.6% of everything the run wrote. We did not run a cluster with checksums off, so that is a share of this run's WAL, not a saving on offer.

Size still matters, through the page count. At the same 100 transactions a second, a 157 MB copy wrote 820 MB in the half hour against the 755 MB copy's 1.67 GB, with nearly the same number of records (1.16 million against 1.24 million). The difference was page images, 92,916 against 199,586: the bigger table spread the same rate of updates over more pages, so more of them were a page's first change since a checkpoint. Your write rate and your table sizes both feed the archive, and the retention setting multiplies whatever they produce.

What RDS, Azure and S3 charge for a 35-day window

They bill different things, which is why the same database can cost nothing on one and real money on another. We ran neither managed service, so what follows is each vendor's own rule, priced from its own price list.

Amazon RDS gives each Region free backup storage equal to the database storage you provision, and charges USD 0.113 a GB-month above that in Cape Town. On what counts, AWS's database blog on backup storage costs is direct: "Transaction log backups required for database point-in-time recovery in the automated backup window don't contribute to backup storage sizing." The RDS user guide describes the charge in snapshot terms too: the free allowance is "spread across all of your snapshots", and you are billed for "the snapshot storage that exceeds your free backup storage allowance". The daily snapshots count, and AWS's estimate for them is data size plus retained snapshots times daily change, minus provisioned storage. AWS's pricing page goes further and says most customers will never pay for backup storage. On RDS the WAL in our figure stays off the invoice by AWS's own account, and the line to watch on your bill is ChargedBackupUsage.

Azure Database for PostgreSQL counts the WAL. Microsoft's backup documentation says that "irrespective of the database size, heavy transactional activity on the server generates more WAL files", which raise backup storage, and its Backup Storage Used metric adds the retained log backups to the snapshots. The free allowance is again your provisioned storage, and South Africa North lists locally redundant backup storage at USD 0.113 a GB-month. Seven days, Azure's default retention, of our run's archive is 558 GB as PostgreSQL writes it. Microsoft's page does not say whether it compresses what it keeps, so the metric on your own server is the number to trust. Every 100 GB it shows over your allowance is USD 11.30 a month, and geo-redundant backup doubles the stored size before the allowance comes off.

An archive you run yourself costs every byte it holds, and it is as readable as the database unless you encrypt it, the same gap a plain MongoDB dump has. On S3 Standard in Cape Town, USD 0.0274 a GB-month up to 51,200 GB, the 35-day archive at stock settings is about USD 71 a month (S3 counts a GB as 1,073,741,824 bytes, so our 2,790 GB bills as 2,599), plus the base backups the window restores from, which at under 1 GB each are a rounding error here.

A 35-day PostgreSQL recovery window, next to the database it protects
A 35-day PostgreSQL recovery window, next to the database it protectsSize of 35 days of archived WAL for a 755 MB PostgreSQL 18 database taking 100 transactions a second, by setting, priced at S3 Standard in Cape Town at 0.0274 dollars per binary GB-month. Stock settings keep 2,790 GB, 15 minute checkpoints with a 4GB max_wal_size 1,945 GB, lz4 WAL compression 705 GB and stock settings with each file compressed by zstd 233 GB, against a 774 MB database. The lz4 and zstd savings were measured on pgbench rows, which are mostly blank padding; on random rows lz4 saved 20 percent and zstd kept 63 percent.35 DAYS OF ARCHIVES3 CAPE TOWNThe database itselfend of the run774 MBStock settingssegment files as archived2,790 GB$71 a monthcheckpoint_timeout 15minmax_wal_size 4GB, files as archived1,945 GB$50 a monthwal_compression lz4files as archived705 GB$18 a monthStock settings, zstd -3each file compressed on the way out233 GB$5.95 a monthSAME SETTINGS ON RANDOM ROWSlz4: 20% less WAL, not 75%zstd: files kept 63%, not 8%
Every setting left the archive hundreds of times the size of the data. At stock settings, 35 days of WAL came to 2,790 GB for a database that ended the run at 774 MB.

The two compression rows were measured on pgbench, which pads every account row with blanks. On random rows the same settings saved far less, so test on your own data.

If you would rather see that sum on your own database than on pgbench, the scoping form asks how far back you need to restore, and you get one monthly figure back with the window in it.

Which settings shrink the WAL archive

Three changes shrank it, by very different amounts, and one setting inflated it.

Checkpoints every 15 minutes instead of 5 (checkpoint_timeout = 15min, with max_wal_size raised to 4GB so the WAL volume did not start one sooner) cut the WAL by 31% (1.16 GB against 1.67 GB), because fewer checkpoints mean fewer first changes. The docs name the price: a longer interval "can increase the amount of time needed for crash recovery."

wal_compression = lz4 compresses the page images as they are written. On pgbench it cut the WAL by 75%, and that number flatters. pgbench's account rows carry a filler column that its own source code describes as a "blank-padded empty string". The same setting on a table of random bytes cut the WAL by 20%. Compressing each archived file with zstd -3 showed the same split: the files kept 8% of their size on pgbench and 63% on random rows. We tested only these two kinds of row, so test on a copy of your own data before you trust either figure.

The setting that inflated it was archive_timeout. It forces a new WAL file after the set interval if anything was written, and the docs warn that a file closed early is "still the same length" as a full one. At 60 seconds and one transaction a second, 28 MB of WAL went to the archive as 31 files of 16 MiB, 496 MiB in all, 18.5 times what was written. Those files shrank to 0.6% of their size under zstd, so compressing each file on the way out takes back nearly all of the padding. With nothing written, no file was forced at all.

An opened hard disk drive in black and white, platter and read arm visible: where a self-managed WAL archive ends up

Measure your own WAL rate before you choose the window

Two readings an hour apart, at a normal busy hour, give you the number our whole calculation hangs on:

SELECT now(), pg_current_wal_lsn();    -- first reading, note the LSN
-- an hour later, with the first LSN pasted in:
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '3A/5C0016E8'));
SELECT wal_fpi, wal_bytes, stats_reset FROM pg_stat_wal;

Multiply the hourly figure by 24, then by the days in the window. On RDS the CloudWatch metric TransactionLogsGeneration reports the same rate in bytes per second without a query, though by AWS's account it is not what you pay for there. On Azure, read the Backup Storage Used metric.

Where the archive sits matters as much as its size. It carries the pages and rows your writes touched, so a Kenyan database archiving to Cape Town or South Africa North has its data outside Kenya. If the data protection rules that bind you want the data in the country, the archive is part of that question, not an afterthought to it.

When the window is worth paying us to price

A database on RDS with a short retention and no ChargedBackupUsage line on the bill does not need us for this. AWS's rule already keeps the WAL off the invoice, and the snapshots are inside your allowance.

The ones that do are a PostgreSQL server you run yourself, in Nairobi or in someone's cloud, whose archive nobody has ever totaled, and an Azure server whose backup line grows every month while the database does not. Send us the PostgreSQL version, the data size, where it runs, the window you need and two WAL readings an hour apart, through the managed database scoping form (the readings go in the notes box) or on WhatsApp at +254 713 403 044. You get back the archive size for that window at your write rate, the settings we would change and what each saves on your kind of data, and one monthly figure for running the database with that window and a restore tested against it. The sum is free, and sending it obliges you to nothing.

WhatsApp