StormaticsStormatics

Your Postgres Database Is Slow, and It Isn’t Postgres

What a 4 TiB Azure Disk Taught Us about Slow Reads

One of our customers spent most of a week tuning PostgreSQL to fix slow reads. They worked through shared_buffers, effective_cache_size, work_mem, and the rest of the checklist, and set all of them sensibly. The reads stayed slow. And the reason had nothing to do with any of those settings. Their data disk had reached 4 TiB, and the moment it did, Azure switched off the host cache sitting in front of it.

That is the frustrating thing about storage. Postgres sits on top of it and trusts it completely. When the disk underneath is capped, throttled, or uncached, Postgres has no way to tell you. It just waits. And from the inside, waiting on a slow disk looks a lot like a database that needs tuning, so that is exactly where people spend their time. But the real problem is a PostgreSQL storage bottleneck, the one place few people think to look.

Here are the four traps we see most often, beginning with the one behind this customer’s slow reads:

Trap One: The Caching Cliff at 4 TiB

On Azure, host caching is a significant performance feature. The VM keeps a cache built from its own memory and a local SSD, and serves a large share of your reads from that cache, so they never travel to the remote data disk. With that cache working, a VM can read faster than the underlying disk could ever deliver on its own. For a read-heavy Postgres workload, that cache is doing a lot of quiet, unglamorous work.

Then the disk reaches 4 TiB, and the cache turns off.

That is the part that catches everyone. Azure supports host caching only on disks smaller than 4 TiB. The moment a single managed disk is 4 TiB or larger, caching disappears, and every read takes the long trip to remote storage. Nothing in Postgres changed. Nothing in your config changed. You simply grew the disk to a size you probably didn’t know mattered, and your read path got slower.

It is easy to miss, because the Azure portal will still show you a cache setting on a disk that large and let you pick one. It just does not take effect. So you can look at the configuration, see “ReadOnly cache,” and reasonably assume caching is on, while every read is actually going straight to remote storage.

The fix is almost boringly simple, and that is the good news. Instead of one giant disk, use several smaller ones, each under 4 TiB, and stripe them together into a single logical volume with LVM or software RAID. Each disk keeps its host cache, the striped volume gives you all the capacity you need, and your reads start coming off cache again. Same data, same Postgres, a smarter disk layout. You get capacity and caching instead of trading one for the other. That was our recommendation for this customer: several smaller striped disks instead of one large one, so host caching could work again.

Trap Two: The VM Has Its Own I/O Ceiling

This trap explains why a bigger, faster disk sometimes delivers no improvement at all.

Your disk has a performance limit. Your VM also has a performance limit, and the two are separate. The virtual machine caps how much disk IOPS and throughput it will pass through, regardless of what the disk underneath can do. Attach a monster disk to a small VM and the VM’s ceiling is what you actually get. The disk sits there half idle while the VM politely refuses to push any harder.

So when you are chasing an I/O bottleneck, you have to check both numbers. What is the disk rated for, and what is the VM rated for? If your measured throughput is sitting right at the VM’s uncached limit, a faster disk buys you nothing. The fix is a larger VM size, or a VM family built for storage-heavy work. This is one of the most common places money gets spent in the wrong direction, upgrading the disk when the VM was the wall all along.

Trap Three: Performance Is Tied to Disk Size

On the classic Premium SSD tiers, performance scales with capacity. A small disk gets a small IOPS and throughput allowance. A large disk gets a large one. Sizes run in fixed tiers, from tiny disks with a few hundred IOPS to the big ones that reach tens of thousands.

The trap is subtle. Sometimes you provision a disk for the space you need and end up starved for IOPS, because the size you picked lands in a tier with a modest performance allowance. Other times you over-provision capacity you will never use, purely to buy the IOPS that come with a bigger tier. Either way, you are letting the capacity number decide your performance number, and those two needs do not always match.

Newer disk types loosen this. Premium SSD v2, for example, lets you set capacity, IOPS, and throughput independently, so you can buy exactly the performance you need without inflating the size to get it. That flexibility comes with its own tradeoffs around caching and configuration, so it is a deliberate choice rather than a free upgrade. The point is to size performance and capacity as two separate decisions, on purpose, instead of letting one drag the other along.

Trap Four: WAL and Data Want Different Disks

Postgres has two very different I/O personalities living in one system. Your data files are read-heavy and random. Your write-ahead log is write-heavy and sequential. Treating them the same way on storage leaves performance on the table.

A read-only host cache is a gift for data files because it serves those random reads from fast local cache. That same cache does nothing useful for the WAL, which is almost entirely sequential writes. So a clean layout puts data files on a disk with a read cache and puts the WAL on its own disk tuned for sustained writes, without a read cache in the way. Separating them also means a burst of WAL activity during heavy writes stops competing with your read traffic for the same disk. Two workloads, two disks, each set up for what it actually does.

How to Tell If The Disk Is Really Your Problem

Before you change anything, confirm where the time is going. This takes a few minutes and saves you from tuning the wrong layer.

Start inside Postgres. Look at pg_stat_activity and watch the wait events. If your sessions are stacking up on I/O waits, Postgres is telling you it is waiting on storage, not on locks or CPU. That is your first signal to look beyond Postgres.

Then step outside Postgres to the operating system. Tools like iostat show you disk utilization, average wait time per request, and how many reads and writes are actually happening. If utilization is pinned near the top and wait times are climbing, the disk is saturated.

Now compare what you are seeing against the rated limits of the disk and the VM. Is your measured throughput near the disk’s limit, or near the VM’s limit? Is host caching actually on, or did you cross the 4 TiB line and lose it without noticing? And do WAL and data files share a disk?  Those four numbers, disk limit, VM limit, and cache state, will almost always tell you which trap you are in.

This order matters for a simple reason. Every one of these problems shows up inside Postgres as slow queries, and slow queries pull you toward Postgres tuning. Checking the wait events and the OS first keeps you honest about whether the database was ever the problem.

Why This Matters to the Business

For a regulated finance workload or a busy SaaS backend, storage latency is not an abstract metric. It is the end-of-day batch that now runs past its window. It is the p99 on a customer-facing query drifting past the number in your SLA. It is a report that used to finish in minutes and now takes an hour, on the same data, because the read path lost its cache.

The fixes above are cheap next to the alternative. No license, no rewrite, no new product.

No license, no rewrite, no new product. A different disk layout, a right-sized VM, a cache setting that actually applies. The expensive version is when you never find the storage problem, so you keep scaling the instance up and tuning Postgres harder, paying more every month to work around a bottleneck a few smaller disks would have solved.

This example captures how Stormatics approaches PostgreSQL performance. Postgres is not the problem here. It is running on a storage layout that holds it back its performance, and it has no way to raise its hand about it. Get the layer underneath right, and the same Postgres you already have gets noticeably faster.

The Takeaway

When the database is slow and every Postgres setting is already right, look under Postgres. Check the disk limit, the VM limit, and the cache state before you touch another parameter. Most of the time, the bottleneck is a storage decision made months ago, for perfectly good reasons, and the fix is a layout change rather than a bigger bill.

FAQ

Does Azure really disable disk caching at 4 TiB?

Yes. Host caching, both read-only and read/write, is supported only on managed disks smaller than 4 TiB (up to 4,095 GiB). A single disk of 4 TiB or larger cannot use host caching, and every read goes to remote storage. The portal may still show a cache setting on a larger disk, but it does not take effect.

How do I get more than 4 TiB of space and keep caching?

Use several disks, each smaller than 4 TiB, and stripe them into one logical volume with LVM or software RAID. Each disk keeps its host cache, and the combined volume gives you the capacity you need. You keep both instead of choosing between them.

Is this only an Azure problem?

No. Every major cloud puts limits at both the disk and the instance level. The exact thresholds and the caching behavior differ, but the lesson is the same everywhere. Check what the disk is rated for, check what the VM or instance is rated for, and confirm any caching is actually active.

How do I know if my bottleneck is the disk and not Postgres?

Look at wait events in pg_stat_activity for I/O waits, then use an OS tool like iostat to check disk utilization and latency. If the disk is saturated or your throughput is hitting the disk or VM limit, the storage layer is the bottleneck, not a Postgres setting.

Should Postgres data and WAL share a disk?

Ideally, no. Data files are read-heavy and benefit from a read cache. The WAL is write-heavy and sequential and does not benefit from a read cache. Putting them on separate disks, each configured for its own workload, improves performance and keeps write bursts from competing with read traffic.


Is slow Postgres costing you money?

Stormatics tunes PostgreSQL from the query planner down to the storage layout, so you get the performance you are already paying for. See how we approach PostgreSQL optimization and scaling, or talk to us about where your database is spending its time.

Leave A Comment