Postgres indexes do not shrink on their own. Every update or delete leaves dead tuples behind, and autovacuum reclaims that space for reuse but rarely returns it to the filesystem. Over months of steady writes, an index can grow to several times the size it needs, and every lookup pays for the extra bloat.
What bloat actually costs you
A bloated index still returns correct results. The damage shows up as slower lookups, more buffer cache pressure, and higher I/O on cloud storage where every extra page read has a real dollar cost. A B-tree index that should fit comfortably in shared_buffers can spill past it once bloat triples its size, turning a cache hit into a disk read on every query.
How to spot it
One query makes bloat visible without guessing:
SELECT relname, pg_size_pretty(pg_relation_size(oid)) AS size FROM pg_class WHERE relkind = 'i' ORDER BY pg_relation_size(oid) DESC LIMIT 10;
Compare that size against what a freshly rebuilt index would take. If the gap is large and growing release over release, bloat is compounding rather than being reclaimed.
Fixing it without downtime
REINDEX CONCURRENTLY rebuilds an index in place without holding the exclusive lock that a plain REINDEX takes, so reads and writes keep flowing while it runs. It costs extra disk space during the rebuild and takes longer than the blocking version, but it is the only safe option on a table serving live traffic.