2026-05-02•9 min read
Backend

PostgreSQL index bloat and how we recovered in production

We woke up to alarm spikes indicating DB CPU utilization at 100% and connection pools maxed out. Our core patient queue table, which handles thousands of status writes every minute, had slowed to a crawl. Query execution times had gone from 4ms to over 2000ms. The system was failing.

The cause was index bloat. In PostgreSQL, updates are modeled as a delete followed by an insert (MVCC). Under heavy writes, this leaves behind 'dead tuples'. If autovacuum cannot keep up, indices grow massive, filled with pointers to dead data. Our index was 12GB larger than the actual table, forcing disk scans instead of in-memory index hits.

The quick fix was REINDEX CONCURRENTLY, which rebuilt the index in the background without locking writes. The long-term fix was tuning autovacuum settings: lowering autovacuum_vacuum_scale_factor to 0.05 and increasing autovacuum_max_workers. We learned that database performance isn't just about indexing; it's about vacuuming discipline.

From a systems perspective, implementing this solution required auditing our telemetry structures. We mapped key transactions across our distributed database queries and evaluated the locking overheads under heavy load. By setting up strict validation rules in Prisma, we isolated runtime query errors before they could trickle up to the client view.

Ultimately, building durable systems means choosing boring abstractions and documenting architectural decisions (ADRs) meticulously. When infrastructure behaves predictably, your team can deploy with high confidence. We enforce these performance and security budgets in our continuous integration (CI) workflows, ensuring that every merge maintains the same standard.

Navigation

Explore more production architectures & case studies.