Skip to content
Guide · Database operations

Benchmarking Time-Series Ingestion Without Misleading Results

An ingestion benchmark is only useful if it answers your question under your conditions. This guide sets out a method for benchmarking time-series ingestion for IoT and building telemetry: define the question and pass criteria first, make the workload realistic (series count, intervals, late and duplicate data, bursts), sweep batch sizes, warm up and run long enough for background work, hold durability settings equal, run queries during ingest, and report enough detail for someone else to repeat it. It describes TSBS and pgbench from their own documentation and contains no performance figures.

Facts checked 27 October 2026 against the vendors' official documentation. Versions covered: PostgreSQL 18 (settings and data-profile SQL run on 18.6 in PGlite 0.5.11); TSBS (GitHub README); InfluxDB OSS v2. Next review due April 2027. Products, licences and prices in this field change quickly; confirm on the vendor's site before you choose. No benchmarks are quoted unless the source and test setup are named.
Short answer
  • Write the question and the pass criteria before you run anything: for example (illustrative values) "sustain the peak point rate plus a two-hour outage backlog, with dashboards under their latency target and data visible within N seconds".
  • Generate or replay data that matches production: series count, sampling intervals, late and duplicate readings, value patterns and bursts after outages. Check the generated data with SQL before you load it.
  • Hold durability equal across candidates. The PostgreSQL documentation says turning off fsync risks unrecoverable corruption after a crash and asynchronous commit can lose the most recent transactions. Our caution: treat a result whose durability settings are unknown or different from yours as not comparable.
  • Run long enough for checkpoints, vacuum, compaction and compression to happen inside the measured window, discard the warm-up, and repeat runs to see the spread.
  • Report throughput over time rather than one number, together with versions, hardware, settings, batch size, writers, dataset profile and query load, so that someone else can repeat the run.
How we know: Research-based from the PostgreSQL 18 documentation (pgbench, asynchronous commit, WAL settings, WAL configuration, populating a database), the Time Series Benchmark Suite (TSBS) README on GitHub and the InfluxDB OSS v2 write optimisation guide, checked 27 October 2026. This page contains no benchmark results of our own and quotes no vendor performance figures. The point counts, durations, outage lengths, batch sweeps and run counts in the examples are illustrative assumptions. Statements that results with different durability, hardware or schemas are not comparable are our methodological caution, not a sourced finding; the sourced parts are the PostgreSQL descriptions of what fsync off and asynchronous commit risk. Two SQL blocks were run on PostgreSQL 18.6 (PGlite 0.5.11, an embedded WebAssembly build): a capture of configuration settings and a profile of a generated dataset; their outputs describe that configuration and that data, not speed, and the settings output (including the shared_buffers buffer count) is an illustrative measurement on a small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly), not a production benchmark and not representative of your server. Vendor guidance is given only as what the vendor reports; editorial judgements are labelled as our view.

What an ingestion benchmark should answer

This guide is for an engineer choosing between candidate databases or designs for telemetry, for example plain PostgreSQL with partitions versus a time-series engine for a portfolio of buildings, or for checking that an existing store will cope when a new site goes live. It covers method only. For the factors that make ingestion fast or slow (batching, indexes, partitioning, cardinality), read time-series database performance; this page is about measuring them honestly.

A useful benchmark answers a specific question with a yes or no and a margin. "How fast is database X?" has no useful answer. "Can this design ingest 20,000 BMS points at their real intervals, plus the backlog from a two-hour gateway outage, while ten dashboards refresh, with every reading durable on commit?" does.

Write these down before the first run (the example values are illustrative)
ItemIllustrative example for a building-analytics platform
Steady-state loadPoint count by interval (for example 2,000 points at 1 s, 18,000 at 60 s), expected growth over the hardware's life
Burst loadBacklog from the longest gateway or broker outage you design for, replayed as fast as the pipeline sends it
DurabilityAcknowledged readings survive a server crash (synchronous commit, fsync on, replica acknowledgement if used)
FreshnessMaximum delay from device timestamp to visible in a query
Concurrent queriesLatest-value tiles, 24-hour trends, a daily report, at realistic concurrency
Pass criteriaSustains steady load with headroom, drains the burst within a set time, freshness and query targets met throughout

Make the workload realistic

A common source of misleading results is data that is easier than production. Match these properties:

  • Series count and cardinality. Use the real number of points and the real tag or label set. Engines that index series can behave differently at 1,000 and at 1,000,000 series; see high-cardinality tags.
  • Interval mix. A few fast points and many slow ones produce a different write pattern from every point at the same rate.
  • Order. Real data is mostly in time order with some late readings from reconnecting gateways. A perfectly ordered load can flatter engines that handle out-of-order writes less efficiently.
  • Duplicates. At-least-once delivery from MQTT QoS 1 or a restarted Kafka consumer resends readings; include them and the de-duplication you will run (for example ON CONFLICT DO NOTHING).
  • Values. Compression depends on value patterns. In our expectation (not a sourced or measured finding), constant or smooth synthetic values can compress better than real sensor noise; replay a captured day of real data where you can.
  • Size at test time. Measure with the table, indexes and partitions at the size you will hold at full retention, not an almost empty database.
  • Payload path. Benchmark the protocol you will use (SQL INSERT, COPY, line protocol, an ingestion connector), through the same network path.
Profile the generated load before using it (run on PostgreSQL 18.6; the 200 points, 2% delay and redelivery rate are illustrative): series, duplicates and share arriving out of order
-- bench_load holds the readings in the order the load generator will send them
CREATE TABLE bench_load (
    arrival_seq  bigint PRIMARY KEY,
    point_id     integer NOT NULL,
    ts           timestamptz NOT NULL,
    value        double precision
);

-- 200 points x 10 minutes at 1 s; 2% of readings delayed by 90 seconds
INSERT INTO bench_load
SELECT row_number() OVER (ORDER BY
           g.ts + CASE WHEN (p + extract(epoch FROM g.ts)::int) % 50 = 0
                       THEN interval '90 seconds' ELSE interval '0' END,
           p) AS arrival_seq,
       p, g.ts, 20 + p * 0.1
FROM generate_series(1, 200) AS p
CROSS JOIN generate_series('2026-10-01 00:00+00'::timestamptz,
                           '2026-10-01 00:09:59+00'::timestamptz,
                           interval '1 second') AS g(ts);

-- redeliver every 200th reading at the end, as a restarted consumer would
INSERT INTO bench_load
SELECT 1000000 + row_number() OVER (ORDER BY arrival_seq), point_id, ts, value
FROM bench_load WHERE arrival_seq % 200 = 0;

WITH ordered AS (
    SELECT point_id, ts,
           max(ts) OVER (PARTITION BY point_id ORDER BY arrival_seq
                         ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS max_ts_before
    FROM bench_load
)
SELECT count(*) AS rows_sent,
       count(DISTINCT point_id) AS series,
       count(*) - (SELECT count(*) FROM (SELECT DISTINCT point_id, ts FROM bench_load) d) AS duplicate_rows,
       round(100.0 * count(*) FILTER (WHERE ts < max_ts_before) / count(*), 2) AS pct_out_of_order
FROM ordered;
Output (a description of the dataset, not a performance figure)
 rows_sent | series | duplicate_rows | pct_out_of_order
-----------+--------+----------------+------------------
    120600 |    200 |            600 |             2.48

Reading the profile

The out-of-order share (2.48%) is higher than the 2% of readings deliberately delayed, because the 600 redelivered copies also arrive after newer readings for the same point. That is the point of profiling: the generator's settings and the data it produces are not the same thing. Compare the profile with the same query run on a day of captured production traffic, and record both in the report.

Batch size and number of writers

Batch size is a client setting that vendor documentation singles out, so treat it as a variable to sweep, not a constant to pick once. The PostgreSQL documentation recommends COPY or many rows per transaction over one transaction per row. The vendor reports, in InfluxData's documentation, an optimal batch size of 5,000 lines of line protocol for InfluxDB OSS v2; that is the vendor's guidance for its engine, a starting point for your sweep rather than a value to copy to other products.

  • Sweep batch size (for example rows per statement or per COPY) and the number of concurrent writers separately, one variable at a time.
  • Use the batch size your real pipeline can produce. A Kafka consumer or MQTT bridge that flushes every second at low point counts cannot send very large batches; a benchmark that does is measuring a different system.
  • Record the end-to-end latency the batching adds. Larger batches can raise throughput, and waiting to fill them delays freshness; measure both rather than assuming either.
  • TSBS loaders expose batch size and worker count as common flags; report the values used.

Warm-up, duration and repeat runs

The pgbench documentation is blunt about short tests: it is easy to produce meaningless numbers, any test that runs for only a few seconds should not be believed, runs should last at least a few minutes, and some cases need hours to get reproducible numbers. For time-series ingestion the reasons are specific:

  • Checkpoints. After each checkpoint, the first change to every data page writes a full page image to WAL, so write volume rises and falls with the checkpoint cycle (by default every 5 minutes or when max_wal_size is reached). A run shorter than several cycles samples only part of it.
  • Background work. Autovacuum, compaction, compression jobs and roll-up refreshes all start some time into a run. The pgbench documentation notes that autovacuum can cause unpredictable changes in measured performance; that is real production behaviour and belongs inside the measured window.
  • Caches and partitions. A new partition or chunk starts empty and small; the steady state arrives once it holds a realistic amount of data and its indexes no longer fit in memory as easily.

Discard a warm-up period, measure for long enough to cover several cycles of each background process, and repeat the whole run several times. Report the spread between runs, not only the best one. The pgbench documentation also warns that reproducing a run exactly, with the same random seed, can hide performance variability; use fixed seeds for debugging and varied seeds for the final runs.

Durability settings: compare like with like

A result can be fast partly because writes are not durable. In PostgreSQL, turning fsync off can cause unrecoverable data corruption after a power failure or crash, and asynchronous commit (synchronous_commit = off) returns success before the commit is flushed, so the most recent transactions may be lost in a crash. Other engines have their own equivalents (acknowledging a write before it is persisted, or before replicas confirm it). Decide what an acknowledged reading must survive, configure every candidate to that standard, and state it in the report. As a caution of ours rather than a sourced finding: if two results were run with different durability settings, do not compare them as like for like.

Capture the settings of every run in the same way you capture results. The output below is from the embedded PostgreSQL build used for the SQL on this site, which runs with fsync off; a write benchmark on it would say nothing about a production server.

Record durability and WAL settings with every run (run on PostgreSQL 18.6 in PGlite)
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('fsync', 'synchronous_commit', 'full_page_writes', 'wal_level',
               'shared_buffers', 'max_wal_size', 'checkpoint_timeout', 'autovacuum')
ORDER BY name;
Output (PGlite 0.5.11 configuration, including the shared_buffers buffer count: an illustrative measurement on a small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly); not a production benchmark and not representative of your server; not a recommendation)
        name        | setting | unit
--------------------+---------+------
 autovacuum         | on      |
 checkpoint_timeout | 300     | s
 fsync              | off     |
 full_page_writes   | on      |
 max_wal_size       | 1024    | MB
 shared_buffers     | 16384   | 8kB
 synchronous_commit | on      |
 wal_level          | replica |

Run the query mix during ingestion

Ingest-only results describe a system that differs from most production telemetry stores. Dashboards, alarm rules and reports read the same tables while data arrives, compete for I/O and memory, and may block or be blocked by background work. The TSBS README states that it benchmarks bulk load performance and query execution performance and does not currently measure concurrent insert and query performance. If that combination is your production case, you need to add it yourself.

  • Run your five most important queries at realistic concurrency while the ingest load runs: latest value per point, a 24-hour trend for one asset, a site roll-up for yesterday, an alarm-rule window.
  • Measure freshness: the delay between a reading's timestamp and the moment a query can see it. Insert canary readings with known timestamps and poll for them.
  • Report query latency as percentiles over the run (for example median and 99th percentile), measured separately for each query type.

Do not benchmark the load generator

  • Pre-generate data. TSBS generates data and queries before the benchmark phases so that results are not affected by generating them on the fly. Do the same, or replay captured traffic from files.
  • Watch the client. The pgbench documentation notes that pgbench can itself become the bottleneck with many client sessions, and suggests running it on a different machine with low network latency, or several instances. Monitor client CPU and network; if the client is saturated, the result measures the client.
  • Same path as production. Include TLS, the connection pooler and the network hop you will have, or state clearly that you left them out.
  • Count what was accepted. Verify row counts after the run. A loader that drops rejected batches can show a high rate for data that never arrived.

Known suites: TSBS and pgbench

Two openly available tools are described here; they are examples, not a ranking or a complete list. Their documentation describes what they do and do not cover:

What the suites document about themselves (checked 27 October 2026)
SuiteWhat it isWorkloadsPoints to note
TSBS (Time Series Benchmark Suite)Go programs to generate data and queries, load them and run queries against several databasesA DevOps use case and an IoT use case simulating a trucking fleet, including out-of-order data and batch ingestion from offline trucksDeterministic data from a seed; data and queries pre-generated; the README says it measures bulk load and query execution and does not currently measure concurrent insert and query. It is hosted in the Timescale GitHub organisation and, per its README, based on a fork of work first made public by InfluxDB; as with any suite maintained by a database vendor, check that the settings used for each database match how you would run it
pgbenchPostgreSQL's general benchmarking programTPC-B-like by default; custom scripts with -f can model an ingestion workloadDocuments good practices: run for minutes or hours, repeat runs, watch for autovacuum effects and for the client becoming the bottleneck

Reading someone else's benchmark

Before trusting a published result, including a vendor's, check: which versions and settings were used and whether durability matched yours; how many series and what interval mix; whether data arrived in order; how long the run lasted and whether background work ran inside it; whether queries ran during ingest; who chose the configuration of each product. Our caution: treat results measured on different hardware, schemas or durability settings as not comparable like for like. The time-series database selection guide shortlists products on features; your own run settles performance.

What to report

A report someone else could repeat
SectionContents
Question and criteriaThe question, the pass criteria, and whether each was met
EnvironmentProduct and version, OS, CPU, memory, storage type and size, network between client and server, cloud instance types if any
ConfigurationEvery non-default setting, durability settings, replication, partition or chunk interval, indexes, compression policy
DatasetSeries count, interval mix, time span, late and duplicate share (profiled), value source (captured or generated), seed
LoadProtocol, batch size, writers, target rate if throttled, burst scenario
ProcedureWarm-up discarded, measured duration, number of runs, what ran concurrently
ResultsAccepted rows per second over time (a chart, not one figure), rejected rows, freshness, query latency percentiles, CPU, memory, disk I/O, bytes on disk at the end, spread between runs
AnomaliesErrors, stalls, restarts, anything excluded and why

Example plan: choosing a store for a building portfolio

A hypothetical team is choosing between monthly-partitioned PostgreSQL and a time-series engine for 40 buildings. Their plan, written before any run (the durations, outage length, number of batch sizes and number of repeats are illustrative choices, not recommendations):

  1. Capture one weekday of real BMS traffic from the existing historian: points, intervals, timestamps and values. Profile it with the SQL above.
  2. Scale it to the five-year point count by cloning sites with new point IDs, keeping the interval mix and late-data share.
  3. Pre-load both candidates to the full raw retention size, with roll-ups in place.
  4. Replay at real speed for six hours with dashboards and the alarm-rule queries running; then simulate a two-hour gateway outage and replay its backlog at pipeline speed.
  5. Sweep batch size at three values the ingestion service can actually produce. Keep fsync and synchronous commit on, or the engine's durable-acknowledgement equivalent.
  6. Repeat the full run three times per candidate; report every run with the template above.

The outcome is a pass or fail against the criteria plus the margin, not a ranking of products in general.

Limitations

  • A benchmark describes one workload on one setup. Changing hardware, schema, version or durability can change the outcome.
  • Synthetic data rarely captures every production quirk; keep a replay of real traffic in the suite.
  • Cloud instances and shared storage vary from run to run; repeat runs and report the spread.
  • Ingestion is one dimension. Our editorial view: operations (backups, upgrades, schema changes), cost and skills can matter as much as a throughput margin. For backups and sizing, see backup and restore and sizing storage for one-second telemetry; for the pipeline side, batch vs stream ingestion.

Frequently asked questions

What is the best time series database benchmark?

Our editorial view: there is no single best one; the most useful is built from your own workload and pass criteria. TSBS is an open source starting point with DevOps and IoT use cases, and pgbench can drive custom scripts against PostgreSQL, but both need your series count, interval mix, durability settings and query load to answer your question.

How long should an ingestion benchmark run?

Long enough to cover several checkpoint cycles and the background work (vacuum, compaction, compression, roll-ups) that production will do, after discarding a warm-up. The pgbench documentation recommends at least a few minutes and notes some cases need hours.

Why are vendor benchmark numbers hard to compare?

They are measured on the vendor's choice of hardware, schema, data, durability settings and queries. Durability settings trade safety for speed (the PostgreSQL documentation describes fsync off and asynchronous commit as faster but able to lose or corrupt data on a crash), so our caution is not to compare figures from different vendors as like for like.

Should I turn off fsync to speed up a benchmark?

Not if the result is meant to describe production. The PostgreSQL documentation warns that turning off fsync can cause unrecoverable data corruption after a crash; benchmark with the durability you will run.

What batch size should I use?

Sweep several sizes that your ingestion service can actually produce, and report the one used with every result. Vendor guidance, such as the 5,000 lines the vendor reports as optimal for InfluxDB OSS v2, is a starting point for that engine only.

Sources

Checked 27 October 2026.

How we research these articles: our editorial method. Research-based from official documentation; SQL examples are run on a local database where the page says so. CodeWithSQL earns nothing from the vendors mentioned.

Building a sensor or telemetry database?

Start with the overview, or compare time-series databases side by side.