Skip to content

Product1 publisher3 min readPublished

Headers and index page splits stretch a 20-byte Postgres sensor row to 95.6 bytes

Timescale measured a 20-byte sensor reading at 95.6 bytes per row on Postgres 16 once tuple headers and one index are counted. Over two years at 10,000 tags, that turns a 12 TB plan into 60 TB, and a loaded pilot table can show the gap before the schema review.

The Product Desk · Product desk

Illustration accompanying Headers and index page splits stretch a 20-byte Postgres sensor row to 95.6 bytes

What happened

  • Timescale ran its sizing tests on Postgres 16.13, using a 2-core box with 128 MB of shared_buffers and 10,000 distinct tags.
  • One index on (tag_id, ts DESC) cost 31.5 bytes per row when built in one pass and 43.4 bytes when live ingest filled it.
  • Changing row width later means rewriting the table under an ACCESS EXCLUSIVE lock, which the guide puts at about two weeks for 631 billion rows.
  • Timescale published a SQL query that reads bytes per row off a loaded pilot table and projects row count, terabytes and monthly growth.

Compiled by The Product DeskSomething wrong?How this is made

Why it matters

  • cost A capacity budget built on the monthly figure covers only month one, because the bill keeps rising by about 2.5 TB every month that retention runs.
  • constraint At production scale the schema is effectively fixed, since a rewrite of about 14 days under an exclusive lock is longer than any maintenance window a plant will grant.
  • decision A pilot sizing only counts if it runs on a loaded table carrying every index that will ship, because leaving the indexes out drops nearly half the bytes.
  • capability Any team with a proof-of-concept table can measure its own bytes per row with built-in Postgres size functions before committing to an engine.

The capacity sheet for a plant historian usually starts with three column types: timestamptz, int and double precision, which add up to 20 bytes a reading [1]. Run the guide's target of 10,000 tags at 1 Hz for 24 months through that sheet [10] and the plan comes out near 12.6 TB [16]. Timescale's guide says the inputs in a sheet like this are usually right. The bytes-per-row figure is the guess [2].

Here's what teams tell themselves: a row costs the sum of its columns, and storage is a monthly number. The guide names both habits [8]. Here's what Postgres does with the row. Each heap tuple carries a 23-byte header and pads to 8-byte alignment twice, once before the first column and again before the float. The page adds a 4-byte line pointer. The row is already 2.6 times the column sum before any index exists [4]. According to the guide, the commonly quoted 2.5x to 3.5x multiplier barely covers the heap [7].

Pilots get the index most wrong. Built in one pass, the test index is the cheap 31.5-byte version. Under real ingest from 10,000 tags, pages split constantly and leaves settle near 65% density [5]. The heap plus one ingest-filled index gives the 95.6 figure [14], and the index is about 45% of it [19]. A second index filled the same way takes the row to 139 bytes [6], or about 88 TB at full retention [18]. The guide says a 50-sensor pilot tells you almost nothing about a plant running 10,000 tags at 1 Hz [12].

The monthly quote hides how storage piles up. At the target rate the table adds about 26.3 billion rows and 2.5 TB a month [17]. By month 24 it holds roughly 631 billion rows [15] and 60.3 TB [16], and each month's bill covers every row ever ingested [8].

The guide says the rewrite rate barely changes with table size, so the pilot figure of 10 million rows in 19 seconds scales straight up [9]. That works out to about 526,000 rows a second, or roughly 14 days for 631 billion rows [20].

The guide comes from Timescale, the company behind TimescaleDB [21]. It pitches the method as a way to learn at design time whether vanilla Postgres can carry the deployment [13]. The vendor has a stake in that answer. The measurement itself runs on stock Postgres: the query reads a loaded table with the built-in pg_relation_size, pg_indexes_size and pg_total_relation_size functions [10]. The guide says readers' numbers will differ from those on its 2-core test box [3].

In my view the sizing check belongs before the schema review, laid out as a 2x2. One axis is where the bytes-per-row figure came from: either pg_total_relation_size on a loaded table carrying every index you will ship [11], or a sum of column widths. The other axis is whether the projected row count at full retention, divided by the rewrite rate measured on the pilot, fits inside your maintenance window. In the measured, rewritable cell, the schema can still change after launch. If the figure is measured but the table is too big to rewrite, the index and engine choices are final at review. A summed figure on a small table needs re-measuring, and a wrong guess costs a short lock. The fourth cell, a summed figure on a table too big to rewrite, is where the guide's plant example sits [2].

What to watch

  • Whether Timescale publishes the ingest-ceiling result its method produces for the 10,000-tag case, since that is the figure that settles whether vanilla Postgres keeps up at 1 Hz.
  • Independent runs of the query on larger hardware or other shared_buffers settings, to see whether 95.6 bytes per row holds outside a 2-core test box.
  • Whether Timescale follows up with per-row figures for the same readings stored in TimescaleDB, the comparison its framing points toward.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories