1193 back ends waiting on an append

theanonymousone1 pts0 comments

1193 backends waiting on an append

1193 backends waiting on an append<br>July 17, 2026We got another Postgres IO incident this week, but this time IORead wasn&rsquo;t the bottleneck. The instance was an AWS RDS on gp3 with 12k provisioned IOPS, and after a few hours EBSIOBalance slowly dropped to 0%.<br>During peak: 9k IOWrite, 6k connections (200 is the baseline), checkpointlag spiked to 4 hours.<br>At the same time, we hit the antiwraparound threshold of 200M and autovacuum was triggered, starting the scan of 237 databases (yes, the data model in this one is interesting - this instance has 10k+ databases).<br>wait_event_type | wait_event | count<br>-----------------+---------------------+-------<br>LWLock | WALWrite | 1193<br>IO | DataFileRead | 194<br>LWLock | pg_stat_statements | 23<br>| | 23<br>IPC | CheckpointStart | 14<br>IPC | BufferIo | 8<br>Lock | transactionid | 4<br>IO | DataFileWrite | 4<br>Client | ClientRead | 3<br>Lock | frozenid | 2<br>IO | WalSync | 1<br>IO | DataFileExtend | 1<br>IO | SlruRead | 1<br>IPC | CheckpointDone | 1<br>1193 LWLock on WALWrite surprised me. This database doesn&rsquo;t receive many writes. Most of the load is doing scans on big tables, so at first I thought: Oh, ok, another bad monday because of sequential scans. But that wasn&rsquo;t it.<br>Why are backend processes being locked behind a simple wal write?<br>My first guess, before I looked at pg_stats, was the autovacuum. But look at that: only two processes locked on frozenid. That wasn&rsquo;t it. DataFileRead also surprised me: 194 against 1193 WALWrite locks. This is basically six to one.<br>Another thing: WALWrite is not an IO wait, it&rsquo;s a lock event. Processes were waiting on each other.<br>I remember I thought: but why? Appending to WAL is not an expensive operation. A WAL record is small, and writing to it is an append. Well, today, a few days after the incident, I had the chance to dig deeper by reading xlog.c in the source code. The mental model after this: we have two paths the insert side and the flush side.<br>The first thing I found was the insert side.<br>* Inserting to WAL is protected by a small fixed number of WAL insertion<br>* locks. To insert to the WAL, you must hold one of the locks - it doesn't<br>* matter which one.<br>We have 8 NUM_XLOGINSERT_LOCKS. The insert itself is two steps:<br>* 1. Reserve the right amount of space from the WAL. The current head of<br>* reserved space is kept in Insert->CurrBytePos, and is protected by<br>* insertpos_lck.<br>* 2. Copy the record to the reserved WAL space. This involves finding the<br>* correct WAL buffer containing the reserved space, and copying the<br>* record in place. This can be done concurrently in multiple processes.<br>Step 1 is serialized, and they say so in ReserveXLogInsertLocation:<br>* This is the performance critical part of XLogInsert that must be serialized<br>* across backends. The rest can happen mostly in parallel. Try to keep this<br>* section as short as possible, insertpos_lck can be heavily contended on a<br>* busy system.<br>But look at what it does under that spinlock:<br>SpinLockAcquire(&Insert->insertpos_lck);

startbytepos = Insert->CurrBytePos;<br>endbytepos = startbytepos + size;<br>prevbytepos = Insert->PrevBytePos;<br>Insert->CurrBytePos = endbytepos;<br>Insert->PrevBytePos = startbytepos;

SpinLockRelease(&Insert->insertpos_lck);<br>That&rsquo;s only three reads and two writes. Not that much work. If this was the problem, I would&rsquo;ve seen LWLock:WALInsertLock. But I didn&rsquo;t.<br>The flush path is more interesting:<br>* WALWriteLock: must be held to write WAL buffers to disk (XLogWrite or<br>* XLogFlush).<br>(...)<br>* info_lck is only held long enough to read/update the protected variables,<br>* so it's a plain spinlock. The other locks are held longer (potentially<br>* over I/O operations), so we use LWLocks for them.<br>So flush is 1 lock, exclusive. Very different from the 8 locks of the insert path. Since there&rsquo;s only one WAL stream and one device, this is where successful inserts fall into. It makes sense.<br>XLogWrite does a pg_pwrite under that lock. pwrite is somewhat cheap since it doesn&rsquo;t persist to disk, but is satisfied once it hits the OS page cache. After that, we get fsync - issue_xlog_fsync, which goes through the block layer, the driver, across the EBS network. So everything is memory except the fsync step. This also makes sense.<br>Out of all those 1193 backends, only one backend was doing something, the other ones are waiting on a futex - D state processes.<br>The EBSIOBalance dropping to 0% doesn&rsquo;t signal anything to the kernel because there were no errors - things were waiting on a lock, that&rsquo;s it. Once EBSIOBalance reached zero, requests beyond the baseline were effectively throttled, increasing fsync latency. From the kernel&rsquo;s point of view the device got slower, and the fsync inside the critical section takes longer, the lock is held longer, and 1193 backends wait longer for the lock.<br>Compare it to the IO:DataFileRead, which is a classical wait event. We had 194 backends issuing a pread, each waiting on their own IO....

rsquo insert lock waiting backends processes

Related Articles