Product1 distinct publisher3 min readPublished
Timescale's bloat guide shows an append-only Postgres table sitting at 22% dead tuples. Nothing on a normal dashboard reports that, so the cost arrives as slow query drift that reads like growth.
The Product Desk · Product desk

Compiled by The Product DeskSomething wrong?How this is made
The person who owns the ingest pipeline gets no line item for any of this. What arrives instead is a p95 that drifts up about 3 ms a week and a nightly VACUUM that takes a bit longer than last month's [4]. In Timescale's sample output, device_metrics_08 reports 22.33% dead tuples across 68 GB of total relation size, with a vacuum that ran minutes earlier [14]. Take the tuple ratio at face value and about 15 GB of that table is space a sequential scan walks at full price (0.2233 x 68 GB = 15.2) [22][3].
Treat 15 GB as a ceiling rather than a measurement. pg_total_relation_size includes indexes, and the live and dead counts are estimates from the cumulative statistics system, which pg_stat_reset() sets back to zero [12]. The byte-exact figure needs pgstattuple, which reads every page and refuses to run against a partitioned parent, so it has to be aimed at a leaf [13]. And n_ins_since_vacuum, the column that separates "ingest is outrunning vacuum" from "nothing has been near this table", only exists on PostgreSQL 13 and later [6].
The freeze cadence is scheduled arithmetic. The insert trigger fires at autovacuum_vacuum_insert_threshold plus autovacuum_vacuum_insert_scale_factor times the row count, defaults of 1,000 and 0.2 [8], so a 500-million-row partition crosses at 100,001,000 inserts [10] and, at 50,000 inserts per second, does that every 2,000 seconds [11]. Timescale rounds it to roughly 100 million inserts, or half an hour [9]. The period is governed by how many rows the partition holds rather than by how much of it changes, and the freezing work produces no dead tuples of its own [7][28].
That is why the percentage alone makes a poor alert. Timescale's instruction is to read the ratio beside last_autovacuum [21]. trades_2026_07 is the biggest table in the sample at 122 GB and sits at 0.23% dead with a fresh timestamp, which the guide reads as the do-nothing case [17][18]. events_raw sits at 12.93% and has logged 3,390 autovacuum runs against trades' 2,214, on about 28% of the size [16][25]; Timescale calls that band the equilibrium of a busy append-only table, holding the line with no headroom for a traffic spike [19]. The audit_log row is a different animal: dead tuples outnumber live by roughly 2.4 to 1 [23], the vacuum that last touched it ran more than three days before the others [24], and its insert count since then falls below even the 1,000-row floor in the trigger formula, so ingest volume will not summon a vacuum there [26].
The grid worth putting on a dashboard, then, has dead_pct on one axis and last_autovacuum freshness on the other, with autovacuum_count as the trend line. A low ratio with a fresh vacuum needs nothing. A ratio climbing past 20% with a fresh vacuum means autovacuum is running and still losing ground, which is where settings work belongs [20]. A high ratio with a stale timestamp is a different investigation, because something is keeping vacuum from running and insert volume will not restart it [26]. Which cell a table lands in decides whether Monday goes to autovacuum tuning or to finding out what is holding vacuum off.
Ranked by verification strength, evidence, and original report placement.
Timescale's Postgres bloat guide states that a table that has never run a single UPDATE can still carry 20% dead space.
The guide says bloat is taught as an update-and-delete problem, which is why it catches high-ingest teams by surprise.
On an append-only table taking 50,000 inserts per second, aborted batches and continuous freezing keep autovacuum busy around the clock; when it falls behind, the pages stay allocated and every sequential scan reads dead space at full price.
The guide says bloat shows up as a p95 that drifts up 3 ms a week and a VACUUM that runs longer each night.
By the time bloat registers as an incident, the table is large enough that fixing it means a VACUUM FULL, a rewrite, or a migration.
n_ins_since_vacuum requires PostgreSQL 13 or later, and exact bloat measurement requires CREATE EXTENSION pgstattuple.
Distinct publishers with included, body-backed reporting in this cluster.
1 article · September 2, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
Your "Index Only Scan" Did 2,847 Heap Fetches: Covering Indexes Are a Vacuum Problem1 distinct publisher
product
One indexed column multiplies the series a sensor table has to track1 distinct publisher
build
Four control planes, one Postgres: a team's case against polyglot persistence1 distinct publisher
security
Miraak moves its command and control into cloud PostgreSQL traffic1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Verifiable mechanics, unverified magnitudes
Split the story in two and it grades differently in each half. The mechanism side is checkable by anyone with a psql prompt — the 23-byte tuple header, autovacuum_vacuum_insert_threshold at 1,000 plus 0.2 times the row count, pgstattuple refusing a partitioned parent — and Timescale flags its own instrumentation's weaknesses instead of hiding them. The number side is a single sample output with no stated origin, and every headline figure in this story descends from it.
No deployment evidence to read
Nobody in this reporting is shown running the diagnosis. The four tables are a teaching specimen — no named cluster, no before-and-after, no count of teams who checked and found 20% dead space. Converting a sample row into evidence of practice would be inventing the thing worth knowing.
Framing slightly ahead of the arithmetic
The premise is sound and the packaging outruns it a little. Rolled-back batches genuinely leave dead heap tuples on a table nobody updates, and the guide is honest that audit_log's 71% is a blocker to clear rather than a tuning failure. But the 22% that anchors the story is one illustrative row, and the roughly 15 GB it implies is an upper bound that quietly counts indexes as heap waste. Overstatement by selection of example, not by exaggeration of claim.
Vendor diagnosing its own market
TigerData's business is Postgres for exactly the workload described: partitioned append-only tables absorbing tens of thousands of inserts a second. The guide is more restrained than that alignment predicts — the levers it names are vacuum_cost_limit and the insert thresholds, plain Postgres, not a product upsell — but it still chooses which specimen appears in the output block and which row is "the one this post is about", and no second party checks either choice.
Confident on how, cautious on how much
We would defend the causal story without hesitation and the numbers not at all. The formula, the abort path, and the tooling constraints are all independently testable. Whether any production partition actually sits at 22.33% dead with a fresh autovacuum timestamp is something only Timescale's illustration asserts, and it is the assertion the headline leans on.