Time-Series Databases: Handling Time-Ordered Data
How to handle time-ordered data with InfluxDB and TimescaleDB — covering downsampling, retention policies, high-cardinality tags, and IoT analytics at scale.
Time-series databases are built for timestamped data like sensor readings and metrics, optimizing for append-heavy writes and time-range queries rather than arbitrary lookups. This post compares InfluxDB, TimescaleDB, and ClickHouse across retention policies, downsampling strategies, and cardinality traps that will wreck your query performance if you ignore them. After reading, you'll know how to plan storage for IoT workloads and when to roll high-resolution data into coarser aggregates.
Time-Series Databases: Handling Time-Ordered Data
Time-series databases are purpose-built for data that changes over time — sensor readings, stock prices, application metrics, IoT telemetry. Unlike relational databases designed for arbitrary queries, time-series databases optimize for append-heavy writes, time-range queries, and aggregations over windows. They handle downsampling automatically, manage data retention policies, and compress data efficiently as it ages. This makes them the natural choice for monitoring, analytics, and any workload where understanding trends over time matters most.
Introduction
Time-series data has specific characteristics. Each data point includes a timestamp. Data arrives in time order. You typically query by time ranges. Old data is often deleted or downsampled after a period.
The access patterns differ from transactional data. You do not update time-series records. You append new points and query ranges. Aggregations over time windows are common. Downsample high-resolution data into lower-resolution summaries.
> INSERT temperature,location=room1 value=22.5 1700000000000000000
> INSERT temperature,location=room1 value=22.7 1700000001000000000
> INSERT temperature,location=room2 value=23.1 1700000002000000000
The measurement is temperature, tags include location, and the value is the reading. The timestamp is nanoseconds since epoch.
Topic-Specific Deep Dives
Retention Policies
Time-series databases handle data lifecycle automatically. You define retention policies that determine how long data is kept at different resolutions.
CREATE RETENTION POLICY "one_day" ON "sensor_data"
DURATION 1d
REPLICATION 1
SHARD DURATION 1h
CREATE RETENTION POLICY "one_year" ON "sensor_data"
DURATION 365d
REPLICATION 1
SHARD DURATION 1w
Raw sensor data might be kept for one day at full resolution. Older data is downsampled into hourly summaries and kept for a year. After a year, it is deleted.
This automatic data management is valuable for IoT and monitoring use cases. You ingest high-resolution data for real-time analysis, and older data automatically rolls up into summaries.
Shard duration and retention interact in ways that catch people off guard. In InfluxDB, when a shard group reaches its end time, the entire group becomes eligible for deletion — not individual points within it. If your shard duration is 1 hour and your retention is 1 day, InfluxDB creates 24 shard groups per measurement. When the retention policy fires, it drops entire shard groups older than 24 hours. Set shard duration to 1 week with 1-day retention, and the behavior breaks down — the shard group spans 7 days but the policy expects to delete after 1 day, so old data lingers until the entire shard group expires. The practical rule: shard duration should be shorter than your shortest retention policy, typically by a factor of 10 or more.
A common setup uses three tiers. Hot data (raw, full resolution, short retention) lives in a policy with 1-hour shards and 1-day retention. Warm data (downsampled to 1-minute or 1-hour intervals) uses 1-day shards and 90-day retention. Cold data (daily aggregates, long-term archive) uses 1-week shards and multi-year retention. Downsampling continuous queries feed data from shorter-retention policies into longer-retention ones, so the query populating the warm tier runs against the hot tier measurement.
Test retention before deploying to production. Create a small test database, write data with known timestamps, set a short retention policy (10 minutes), and wait. Verify that data disappears exactly at the retention boundary. This catches bugs where the retention policy is specified in days but interpreted in nanoseconds, or where shard groups span retention boundaries in a way that delays deletion. Run this test on every major version upgrade — retention behavior has changed between InfluxDB releases.
Modifying retention on live systems works differently than you might expect. In InfluxDB, ALTER RETENTION POLICY only affects data written after the alteration. Shorten retention from 90 days to 30 days, and data older than 30 days at the time of the ALTER stays until the next compaction cycle evaluates it. To reclaim space immediately, drop the old shard groups manually with DROP SHARD. In TimescaleDB, retention is managed via chunk exclusion. Configure a policy that drops old chunks and verify the chunk disappears from timescaledb_information.chunks after the policy runs.
Downsampling and Data Aggregation
Downsampling reduces data resolution over time. A million raw points become a thousand hourly averages. Queries run faster on the downsampled data.
SELECT mean(value) AS value
INTO temperature_hourly
FROM temperature
WHERE time > now() - 7d
GROUP BY time(1h), location
This query computes hourly averages and writes them to a new measurement. You would run this as a continuous query that runs periodically, automatically downsampling incoming data.
TimescaleDB uses a similar approach with continuous aggregates:
CREATE MATERIALIZED VIEW temperature_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS hour,
location,
avg(value) as avg_temp
FROM temperature
GROUP BY hour, location;
The materialized view is automatically refreshed based on a refresh policy.
Query Performance Considerations
Time-series queries have common patterns. Filtering by time range first narrows the data significantly. Then aggregation across that subset.
Indexing strategies differ from transactional databases. The timestamp is the primary access pattern, so time-based partitioning is key. Tag indexes help filter within time ranges.
Compression reduces storage significantly. Time-series data often compresses well because adjacent points are similar. InfluxDB and TimescaleDB both handle compression automatically.
Specific optimizations that matter in practice:
The order of operations in a query plan determines performance. In InfluxDB, a query like SELECT mean(value) FROM temperature WHERE time > now() - 7d AND location = 'room1' GROUP BY time(1h) executes the time filter first, then the tag filter within that time range. If you reverse the order in the WHERE clause, it makes no difference — InfluxDB’s query planner applies them optimally regardless. However, if your tag cardinality is too high, tag filtering becomes expensive even within a narrow time window.
TimescaleDB benefits from PostgreSQL’s query planner, but the chunk interval determines how much data a query scans. A query against a hypertable with daily chunks and a 7-day time range hits 7 chunks. With hourly chunks over the same range, it hits 168 chunks. More chunks mean more overhead in the query planner’s partition pruning. For queries that always filter by week-long windows, daily chunks are the sweet spot.
For dashboards hitting the same queries repeatedly, caching matters. InfluxDB’s SHOW CARDINALITY command tells you how many series exist — high cardinality means the series index is large and lookups take longer. TimescaleDB’s chunk_sizing_info() function reveals whether your chunk interval matches your query patterns.
ClickHouse query performance depends heavily on the max_threads setting and how data is partitioned. Queries against ClickHouse with proper time-range partitioning see the query read only the partitions overlapping the requested time range, skipping irrelevant data entirely. A poorly partitioned ClickHouse table can scan the entire dataset on every query.
Monitor query latency percentiles, not averages. A query with p50 of 10ms but p99 of 8 seconds indicates a worst-case problem — likely a query hitting an edge case with a very wide time range or high cardinality tag combination. Set up alerting on p99 query latency to catch these before they affect users.
Time-Series Database Comparison
| Feature | InfluxDB | TimescaleDB | QuestDB | ClickHouse |
|---|---|---|---|---|
| License | MIT (OSS), commercial cloud | Apache 2 (OSS), TimescaleDB Cloud | Apache 2 | Apache 2 |
| SQL support | InfluxQL + Flux | Full SQL (PostgreSQL) | Full SQL | Full SQL |
| Write throughput | Very high (100k+ pts/s) | High (tied to PostgreSQL) | Very high (100k+ pts/s) | Very high (100k+ pts/s) |
| Query performance | Fast for time-range | Fast with chunking | Very fast columnar | Excellent for analytics |
| Compression | TSM engine (built-in) | PostgreSQL-based | Columnar, highly optimized | Best-in-class columnar |
| Downsampling | Continuous queries | Continuous aggregates | Parititioned by time | Materialized views + clickhouse-kafka |
| Retention policies | Native, automatic | Native via chunking | Manual | Manual via TTL |
| Ecosystem | Telegraf, Flux, cloud | PostgreSQL tooling | Kafka, Telegraf, Python | Kafka, Superset, Grafana |
| Operational complexity | Low (single binary) | Medium (PostgreSQL-based) | Low (single binary) | Higher (cluster setup) |
| Best for | Pure TSDB workloads, metrics | Existing PG teams needing TSDB | Ultra-low latency, Kafka ingestion | Analytical heavy-lift with time-series |
Time-Series Database Architecture Comparison
flowchart LR
subgraph InfluxDB["InfluxDB"]
W1["WAL"] --> C1["Cache"]
C1 --> T1["TSM Files"]
end
subgraph TimescaleDB["TimescaleDB"]
W2["PostgreSQL WAL"] --> C2["Chunk Table 1"]
W2 --> C3["Chunk Table N"]
end
subgraph ClickHouse["ClickHouse"]
W3["Partition Buffer"] --> P1["Sorted Parts"]
P1 --> M1["Merged Parts"]
end
I["Ingest"] --> W1
I --> W2
I --> W3
T1 --> Q["Query"]
C2 --> Q
C3 --> Q
M1 --> Q
| Aspect | InfluxDB | TimescaleDB | ClickHouse |
|---|---|---|---|
| Write path | WAL + cache flush | PostgreSQL WAL + indexes | Append buffer + merge |
| Data organization | TSM time-partitioned | PostgreSQL chunks | Columnar partitions |
| Query executor | InfluxQL/Flux | PostgreSQL planner | SQL + ClickHouse func |
| Partition strategy | Shard groups by time | Hypertables by time | Manual time partitions |
| Compaction | Background TSM merge | PostgreSQL VACUUM | Background part merge |
Use Cases
IoT Sensor Data
IoT deployments generate massive amounts of time-series data. Temperature, humidity, pressure, motion. Sensors report readings every few seconds. You need to store this, query it, and visualize it.
Time-series databases handle the write volume and query patterns. You can store millions of points per second. Queries aggregate across time ranges efficiently.
Real-world IoT data volumes and schemas:
A typical industrial IoT deployment has 10,000 to 100,000 sensors. Each sensor reports 1-5 readings per second. At 10,000 sensors reporting 1 reading per second with 8 fields per reading, you are writing 80,000 points per second — roughly 6.9 billion points per day. Stored at 50 bytes per point, that is 345GB of raw data per day before compression. InfluxDB compresses this to 35-70GB on disk. A single-node InfluxDB instance handles this comfortably; TimescaleDB on a well-tuned PostgreSQL instance handles 50,000-100,000 points per second per node.
Schema design for IoT follows a consistent pattern. The measurement name identifies the sensor type (temperature, pressure, vibration). Tags hold metadata that filters queries: device_id (low cardinality only — use device type, region, or customer tier instead), location, device_type, firmware_version. Fields hold the actual readings: value, unit, quality. Timestamps come from the sensor gateway or the database, depending on whether you need to preserve the sensor’s local clock.
Protocol considerations:
Most IoT sensors use MQTT to publish data to a broker like Mosquitto or HiveMQ. The broker forwards data to Telegraf (for InfluxDB/QuestDB) or directly to the database. For high-volume environments, batching MQTT messages into groups of 1000-5000 points before writing cuts network overhead dramatically. Telegraf’s mqtt_consumer plugin handles this with configurable batch sizes.
For TimescaleDB, the timescaledb-parallel-copy utility parallelizes bulk inserts from CSV or line protocol files, achieving 10x the throughput of single-threaded COPY commands for large imports.
If sensors report over unreliable networks, implement a local buffer on the gateway. When the connection restores, replay buffered data in time order — out-of-order writes hurt compression ratios. The buffer size depends on expected outage duration: a 1-hour outage at 80,000 points per second needs a 288MB buffer per gateway.
Application Metrics
Application performance monitoring generates time-series data. Request rates, latency percentiles, error rates, resource utilization. Tools like Prometheus use time-series databases under the hood.
Dashboards query metrics by time range, aggregate across services, and correlate across metric types. Time-series databases support the query patterns monitoring tools need.
Financial Data
Stock prices, trade volumes, order book deltas. Financial data is naturally time-series. High-frequency trading systems generate enormous volumes. Analytics queries aggregate across time windows.
TimescaleDB handles financial data well when you need SQL capabilities alongside time-series optimization. InfluxDB excels for pure write throughput and downsample-heavy workloads.
Capacity Estimation: Retention, Partitioning, and Write Throughput
Sizing a time-series database starts with write throughput. InfluxDB handles roughly 100k-500k points per second per node depending on hardware and compression. If you have 10,000 sensors reporting every 5 seconds, that is 2,000 points per second — well within a single node’s capacity. If you have 1 million meters reporting every second, that is 1 million points per second, which needs a cluster.
The storage formula: points per second times average point size times retention duration. A sensor sending 10 fields at 50 bytes per point, 1 point per second, kept for 90 days: 1 50 90 * 86400 = 388MB per sensor. Scale that to 100,000 sensors and you need roughly 38.8GB of raw storage. With compression (InfluxDB typically achieves 3-10x compression on time-series data), that shrinks to 4-13GB.
TimescaleDB chunk sizing matters for performance. TimescaleDB splits data into chunks by time interval. The chunk interval determines how much data sits in each partition. Too-large chunks defeat the purpose of partitioning — queries scan too much data. Too-small chunks create too many partitions and cause overhead. The rule of thumb: target 1-10 chunks per query for the time ranges you typically query. For 90-day retention querying data in 7-day windows, hourly chunks (2160 chunks total, ~4 per query on average) work well.
For TimescaleDB, estimate memory as: chunk_size * active_chunks_in_memory * row_width. If each row is 100 bytes and you keep 7 days in memory across 100 concurrent queries, that is roughly 100 7 86400 100 100 = 6.9GB. Add 30% for index overhead if you have indexes on tag columns.
InfluxDB TSM file sizing: each TSM file covers a shard group time range. With 1-hour shard duration and 7-day retention, each TSM file holds 1 hour of data. For 10,000 points per second, that is 36 million points per TSM file. At 50 bytes per point compressed, each TSM file is roughly 1.8GB compressed on disk. Monitor diskdatan and diskn metrics to track actual sizes.
Trade-off Analysis
When selecting and operating a time-series database, several key trade-offs determine the right choice for your workload:
| Trade-off | InfluxDB | TimescaleDB | ClickHouse |
|---|---|---|---|
| Write throughput vs SQL compatibility | Higher write throughput, proprietary InfluxQL/Flux | Full SQL via PostgreSQL, lower write throughput | High write throughput, full SQL |
| Operational complexity | Single binary, low complexity | PostgreSQL-based, medium complexity | Cluster setup required, higher complexity |
| Compression vs query flexibility | Good compression, limited query types | Good compression via PostgreSQL, full SQL flexibility | Best-in-class columnar compression, analytical focus |
| Latency vs durability | WAL caching, potential data loss risk | PostgreSQL durability, fsync on write | Configurable durability, merge strategy |
| Downsampling vs precision | Native continuous queries, precision loss at write time | Materialized views, refresh policies | Manual materialized views, configurable refresh |
Key decision framework:
- Choose InfluxDB when write throughput is critical and your team can work with InfluxQL/Flux
- Choose TimescaleDB when you need PostgreSQL compatibility and have existing SQL expertise
- Choose ClickHouse when analytical query performance is paramount and you can manage operational complexity
The biggest mistake is choosing based on popularity rather than fit for your specific access patterns and team capabilities.
When to Use / When Not to Use
When to Use Time-Series Databases:
- You have high-volume data with timestamps where timing is essential
- Your queries aggregate data over time windows (hourly averages, daily max/min)
- You need automatic data retention and downsampling policies
- You are building IoT sensor monitoring, application performance monitoring, or financial analytics
- Write throughput is more important than arbitrary querying flexibility
- You need specialized time-series functions like gap filling, interpolation, or smoothing
When Not to Use Time-Series Databases:
- Your data has complex relationships requiring multi-table joins
- You need arbitrary ad-hoc queries across non-time dimensions
- Your use case is primarily single-record lookups by ID
- You need strong transactional guarantees across multiple entity types
- Your data volume is low and a regular relational database handles it fine
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Write throughput exceeding capacity | Data loss, dropped points, increased latency | Implement buffering, batch writes, scale ingestion horizontally |
| Retention policy misconfiguration | Accidental data loss or excessive storage usage | Test policies on small datasets first, implement backups before changes |
| Compaction backlog | Query performance degrades over time | Monitor compaction progress, tune compaction settings, scale resources |
| Query timeout on large ranges | Dashboard timeouts, incomplete data views | Implement query timeouts, use downsampled data for long ranges, paginate |
| Timezone handling bugs | Incorrect aggregations, data aligned to wrong periods | Normalize to UTC internally, handle timezone conversion at query time |
| High cardinality tag explosion | Memory exhaustion, slow queries | Limit unique tag values, use coarser-grained tags, implement tag cardinality limits |
| Shard/index corruption | Missing or incorrect query results | Enable checksums, regular integrity checks, replica verification |
| Downsampling continuous query failure | Incomplete aggregated data | Monitor continuous query status, implement alerting on query errors |
Common Pitfalls / Anti-Patterns
-
High cardinality tag values: Using user IDs, email addresses, or other high-uniqueness values as tags causes memory issues. Tags should have bounded cardinality (thousands, not millions of unique values).
-
Not using bulk writes: Writing point-by-point is extremely inefficient. Batch writes into groups of 1000-5000 points for optimal throughput.
-
Ignoring retention policies: Without proper retention, storage grows unbounded. Define retention upfront and test that it actually deletes data.
-
Querying too wide time ranges: Queries spanning years cause timeouts. Use downsampled data for long ranges, only query raw data for recent windows.
-
Storing non-time-series data: Forcing unrelated data into a time-series model creates awkward schemas. Use the right tool for each data type.
-
Not understanding compaction behavior: Compactions cause I/O spikes and temporary query slowdowns. Schedule intensive compactions during off-peak hours.
-
Underestimating downsample precision loss: Aggregated data loses granularity. Ensure your downsampling strategy preserves the precision your queries need.
-
Forgetting about timezone handling: Storing local times without timezone info causes subtle bugs when aggregating across regions.
Failure Scenarios and Trade-off Analysis
Write Amplification and Compaction
Time-series databases suffer from write amplification — one logical write becomes multiple physical writes during compaction.
flowchart TD
W["Write"] --> WAL["WAL Write"]
WAL --> Cache["MemStore/Cache"]
Cache --> Flush["Flush to TSM/Chunk"]
Flush --> C1["L1 Compaction"]
C1 --> C2["L2 Compaction"]
C2 --> C3["L3 Compaction"]
The compaction pipeline means data is rewritten multiple times before reaching its final compressed form.
Write amplification has real hardware consequences. Each physical write to an SSD consumes one of the drive’s limited program/erase cycles. A write amplification factor of 10x means every logical write you send to InfluxDB translates to 10 flash writes. On a busy time-series database writing 100GB per day of logical data, a 10x amplification factor burns through 1TB of SSD endurance per day. On a 100TB SSD with 10,000 write cycles endurance, that works out to roughly 100 days before the drive fails. Monitoring SSD wear is not optional for high-volume InfluxDB deployments — many hosted time-series services expose SMART metrics that include wear indicators.
InfluxDB’s max-concurrent compactions setting controls how many compaction processes run simultaneously. The default matches the number of CPU cores. Set it too high and compaction competes with queries for I/O bandwidth and CPU, causing query latency spikes. Set it too low and compaction falls behind the ingest rate, disk usage grows unbounded as TSM files accumulate in early-stage compaction levels, and reads slow because they must scan more TSM file headers. A healthy compaction ratio keeps level 1 files from growing faster than level 2 files can catch up. TimescaleDB compaction uses PostgreSQL’s native compression policies, controlled via timescaledb.compression with a configurable compress_interval that determines how far back compression runs.
Watch the disk and diskn metrics from show stats in InfluxDB. The disk metric is the total size of all TSM files on disk. If disk usage grows faster than your retention policy deletes data, compaction is falling behind. A healthy system reaches a steady state and stays there. Also watch the distribution of TSM files across compaction levels — if most files are stuck at level 1 and level 2 is not increasing, the compaction pipeline is stalled. For TimescaleDB, timescaledb_information.chunks shows chunk compression status. A build-up of uncompressed old chunks means the compression policy is not keeping up. The timescaledb_information.jobs view shows background job status including last run time and any errors.
Heavy compaction improves read performance because compressed data decompresses faster and fewer files need to be opened. But running compaction during peak query hours causes I/O contention and query latency spikes. Schedule intensive compaction during off-peak maintenance windows. In InfluxDB, compact-full forces full compaction of all TSM files, but run it during maintenance windows only — it is I/O intensive and will degrade query performance while running.
High Cardinality Tag Design Patterns
High cardinality is the most common performance killer in time-series databases.
# BAD: High cardinality tag (millions of unique values)
# User IDs, device IDs, session IDs as tags
measurement,device_id=abc123,value=10 # 1M series
# GOOD: Low cardinality tag (thousands of unique values)
# Device type, region, customer tier as tags
measurement,region=us-west,device_type=sensor,value=10 # 1000 series
Cardinality budget: If you have 10 tag dimensions with 100 unique values each, you create 100^10 possible combinations — but only a small fraction exist in your data. If your total series count exceeds 10 million in InfluxDB, performance degrades.
Cross-Platform Migration: InfluxDB to TimescaleDB
Migrating from InfluxDB to TimescaleDB is a common path when teams outgrow InfluxDB’s single-node limitations or when they need PostgreSQL’s full SQL capabilities alongside time-series data. The migration has three distinct phases: exporting the data, transforming the schema, and importing with appropriate chunk sizing.
Use influx inspect export to dump line protocol, but understand what it does not export. The tool only exports data flushed to TSM files, not data sitting in the WAL or cache. Capture recent data by halting the write pipeline and forcing a flush first. Export in time-chunked batches, oldest periods first. Trying to export years of data in a single invocation produces files too large to manage and creates no recovery point if the process fails. A practical batch strategy: export in 30-day chunks using --start and --end flags. Keep each export file under 10GB compressed.
InfluxDB’s measurement-tag-field model maps to a TimescaleDB hypertable where tags become columns and fields become additional value columns. A field with multiple value types (string, float, integer) needs separate columns in the relational model. The timestamp column becomes the time dimension. Low-cardinality tags become regular indexed columns. High-cardinality tags that would have caused problems in InfluxDB (like device IDs) become columns with B-tree indexes in TimescaleDB — the cardinality problem is less severe in the PostgreSQL model because indexes are not inverted indexes. Pre-create the hypertable with the correct chunk interval before importing. For data spanning multiple years with 90-day retention, use 90-day chunks.
The cleanest migration approach uses dual-write during the transition. Redirect new writes to TimescaleDB while the InfluxDB export is running. Then do a final delta sync of any data written during the export process. The delta sync step matters because the export tool does not capture WAL data. A 50TB compressed InfluxDB export typically takes 2-4 weeks for the initial export, followed by a short delta-sync window (minutes to hours depending on write rate). During the delta-sync window, the application writes to both systems. Once TimescaleDB has caught up, switch the application to write to TimescaleDB only and decommission the InfluxDB instance.
# Export InfluxDB line protocol
influx inspect export \
--engine-path /var/lib/influxdb \
--output-path /tmp/export.lp
# Import into TimescaleDB
timescaledb-parallel-copy \
--connection "host=localhost user=postgres db=timeseries" \
--file /tmp/export.lp \
--batch-size 10000 \
--workers 4
Interview Questions
Storage math first: 50,000 sensors × 1/minute × 60 minutes × 24 hours × 90 days = 6.48 billion points. At roughly 50 bytes per point uncompressed, that is 324GB raw. With 5x compression (typical for time-series), you are at roughly 65GB on disk. That fits easily on a single node for both InfluxDB and TimescaleDB.
For the technology choice: if your team knows PostgreSQL and needs features like full-text search, JSON support, or complex joins across sensor metadata, TimescaleDB is a natural fit. If you need maximum write throughput and are comfortable with InfluxQL or Flux, InfluxDB handles this write volume easily. The deciding factor is usually team expertise and whether you need PostgreSQL compatibility. At 50,000 sensors and 1 reading per minute, you are well within single-node capacity for either.
The most likely cause is that you are querying raw data across too wide a range. If you have 1-minute resolution data kept for 90 days and you query 30 days at a time, you are scanning 43,200 minutes of data per series. If you have thousands of series in a single query, that is tens of millions of points to decompress and aggregate.
The fix is downsampling. Create a continuous query that rolls up your 1-minute data into 1-hour or 1-day aggregates. Query the downsampled data for long ranges and only hit raw data for recent windows. Alternatively, if you cannot downsample, check whether your tag set is causing high cardinality — too many unique tag values force InfluxDB to scan more series than necessary even within a time range.
InfluxDB's TSM engine organizes data by time into TSM files, each covering a shard group time range. Within each TSM file, data is sorted by measurement, tag set, and field, then compressed. Queries read only the TSM files that overlap with the requested time range. TimescaleDB's chunking is similar but built on PostgreSQL table partitioning — each chunk is a separate PostgreSQL table, and the TimescaleDB hypertable is a view over all chunks. Queries against a time range hit only the relevant chunks, and the PostgreSQL query planner handles it.
The operational difference: TimescaleDB chunks are PostgreSQL tables, so you can query them with standard SQL, join them to other PostgreSQL tables, and use PostgreSQL tooling. InfluxDB TSM files are proprietary and opaque — you cannot query them directly and must use InfluxQL or Flux.
Store all timestamps in UTC internally. This eliminates a whole class of bugs where local time zones cause aggregation misalignment. Most time-series databases store timestamps in UTC by default. At query time, use the timezone parameter in your query language to convert to the display timezone. In InfluxQL: GROUP BY time(1h, TZ('America/New_York')). In TimescaleDB: time_bucket('1 hour', time AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York'). Never store local time zones in the database — it creates edge cases around daylight saving transitions that are painful to debug.
The core tension is between tenant isolation and cross-tenant analytics. Two main approaches:
Separate databases per tenant: Cleanest isolation, natural backup and restore boundaries, but cross-tenant queries require external aggregation. Good for high-security tenants or when tenant count is bounded (dozens, not thousands).
Shared database with tenant tag: Store tenant_id as a tag in InfluxDB or a partition key in TimescaleDB. Query performance relies on tag/cardinality management. With TimescaleDB, use tenant_id as a partition dimension alongside time. Cross-tenant aggregates become range scans on the tenant_id tag — acceptable if tenant count is manageable and you have downsampled data for long-range analytics.
For InfluxDB specifically, keep tenant_id as a low-cardinality tag (bounded set of tenants, not unbounded device IDs). If you have 500 tenants each with 10,000 devices, the tag cardinality is 500 — manageable. If you try to make device_id a tag, you have millions of series and query performance collapses.
InfluxDB continuous queries run on a schedule (every hour, every 5 minutes) and execute a SELECT INTO query against the database. They run inside the InfluxDB process, so there is no network overhead. They are triggered by time, not by data arrival.
TimescaleDB continuous aggregates are implemented as materialized views that automatically refresh based on a policy. TimescaleDB tracks the last refresh time and only recomputes rows in the interval since the last refresh — more efficient than a full recompute.
Why write-time downsampling beats read-time: When you query raw data and aggregate at read time, you pay the decompression and scan cost on every query. If 100 users query 30-day ranges, you decompress the same data 100 times. With write-time downsampling, you compute once (when data arrives) and store compact summaries. Every subsequent query is fast because it reads pre-aggregated data.
The tradeoff: write-time downsampling means you decide the aggregation granularity upfront. If you later need 15-minute resolution but only downsampled to 1-hour, you cannot recover the 15-minute data. Read-time aggregation preserves full flexibility but costs more at query time.
Days 0-1 (raw ingestion): 1 reading per 5 seconds = 720 readings per hour = 17,280 per day. At 50 bytes per point, raw storage grows about 864KB per day per sensor. For 10,000 sensors, that is roughly 8.6GB per day.
Day 1 onward (TSM compaction): InfluxDB flushes the cache to TSM files. Background compaction merges small TSM files into larger ones, applying compression. TSM compression on temperature data (repetitive timestamps, similar values) typically achieves 5-10x. That 8.6GB might compress to 1-2GB on disk.
Day 90 (retention boundary): The 90-day retention policy kicks in. At the exact 90-day mark, InfluxDB drops the oldest TSM files. The data is gone — not archived, not moved. Make sure your downsampled data has already been written to the hourly measurement before this happens.
Post-90 days (downsampled only): You have 1 reading per hour instead of 720. That is 24 points per day instead of 17,280 — a 720x reduction. Storage for 90 days of hourly data is roughly 864KB * 24 / 720 = ~29KB per sensor per 90 days. A massive saving.
High cardinality means a tag or index has many unique values. In InfluxDB, each unique measurement + tag set combination is a series. If you have 1 million device IDs as a tag and each device sends 10 fields, you have 10 million series. InfluxDB must maintain an in-memory series index — too many series exhaust RAM and slow down queries.
Common causes of high cardinality: Using user IDs, session IDs, email addresses, or transaction IDs as tags. These have millions of unique values. Storing GPS coordinates as separate lat/lon tags with high precision. Using UUIDs as tag values.
Design patterns to avoid it: Encode coarse-grained dimensions instead of fine-grained ones. Instead of device_id as a tag, use device_type or region. Instead of exact lat/lon, use geohash cells. Instead of user_id, use user_segment or account_tier. The goal is tags with thousands of unique values, not millions.
If you genuinely need per-device queries, consider TimescaleDB where device_id becomes a column with a B-tree index — different tradeoffs than InfluxDB's inverted index model.
InfluxDB write path: WAL write + in-memory cache index → acknowledgment. No fsync to TSM files on every write. Achieves 100k-500k points per second per node. If you need maximum write throughput and are comfortable with InfluxQL, InfluxDB is fastest for pure time-series inserts.
TimescaleDB write path: PostgreSQL write + WAL + B-tree index updates on every insert. Indexes on tags cause overhead — each write must update the tag index. Throughput is lower than InfluxDB but still high (tens of thousands of points per second). Better if you need SQL compatibility and have complex metadata relationships.
ClickHouse write path: Append-only columnar storage. Writes go to a partition buffer, then get merged into sorted parts. Very high throughput for analytical workloads (100k+ pts/s). Best for analytical queries on time-series data with complex aggregations. Higher operational complexity due to cluster setup.
Decision framework: Choose InfluxDB for pure metrics collection with simple tag filtering. Choose TimescaleDB when you have PostgreSQL expertise and need to join time-series data with relational metadata. Choose ClickHouse when your workload is analytical-heavy and you have the operational maturity to manage a distributed columnar database.
Write-side signals: Monitor ingest rate (points/second) and watch for sustained drops below your baseline. Write latency p99 creeping upward. WAL file size growing — if the WAL is not flushing to TSM files fast enough, it means compaction is falling behind. A growing WAL eventually exhausts disk space.
Query-side signals: Query latency p99 is the most important signal. A rising p99 means the system is struggling to decompress data or is scanning too many chunks. If query latency is stable but user-facing dashboards are slow, check whether they are querying raw data for long ranges when downsampled data exists.
Storage signals: Disk usage growth rate. If disk growth outpaces your retention policy deletion rate, the compaction backlog is growing. Compression ratio dropping (diskn/diskd metrics in InfluxDB) suggests the TSM files are not compacting properly.
TimescaleDB-specific: Monitor chunk count — too many chunks (thousands) means partition overhead is dominating. Check timescaledb_information.jobs for continuous aggregate refresh lag. If the background job has fallen behind, your downsampled data is stale.
Migration strategy: Use the InfluxDB export functionality (influx inspect export) to export cold data as line protocol. Export in time-order chunks — oldest first. This gives you manageable file sizes (do not try to export 2 years in one shot).
Parallel ingestion: TimescaleDB can use COPY or \COPY for bulk loading. Pre-create the hypertable with the correct chunk interval before importing. For 2 years of data with 90-day chunks, you need roughly 8-9 chunks. Use timescaledb.parallel_copy extension if available for faster ingestion.
Downtime window: The cleanest approach is dual-write during migration — write to both InfluxDB and TimescaleDB for the migration period. Then do a final sync of any data written during migration, switch the application to TimescaleDB, and decommission InfluxDB.
Validation: Compare row counts between systems for each time range. Spot-check aggregations (sum, mean, min, max) on sample measurements to confirm values match within tolerance. Check that timestamps are preserved exactly.
InfluxDB: Out-of-order writes go through the normal write path but may land in a different TSM file than expected (based on timestamp). InfluxDB has a out_of_order write option in recent versions that allows batching out-of-order points. The impact is that data with timestamps far in the past does not compress as well because similar timestamps are not grouped together.
TimescaleDB: PostgreSQL handles out-of-order inserts as normal inserts. The chunk containing that timestamp receives the data. If timestamps are truly historical, they land in older chunks that may already be compressed. This can cause index bloat and worse compression ratios.
Storage efficiency implication: Out-of-order data harms compression because time-series compression relies on similarity between adjacent timestamps and values. Random late-arriving data breaks compression runs.
Best practice: Buffer writes on the client side and batch in time order. If late data is unavoidable (sensor network delays, retry queues), batch and sort before writing.
Single-node fits: Write throughput under 500k points/second, storage under 10TB, query latency not critical for all queries. Single-node InfluxDB handles millions of series with proper tag cardinality management.
Signs you have hit single-node limits: Write latency p99 climbing despite batching. Query latency p99 degrading even on optimized queries. Disk I/O saturation (high wait times). Memory pressure causing OOM kills. Specifically for InfluxDB: series cardinality exceeding 10 million per node.
Cluster triggers: Writes consistently above 500k pts/s. Need for read replicas for query parallelism. Storage needs above 10TB on a single volume. Need for cross-node query distribution. HA requirements that demand redundancy.
InfluxDB cluster: Uses shard groups distributed across multiple data nodes. Query engine distributes work across nodes. Higher operational complexity but linear scaling.
TSM compaction: InfluxDB merges small TSM files into larger, more compressed files. Runs continuously in background. Compaction levels: level 1 (small files merge), level 2 (larger files), level 3 (full compaction). Tuning: set max-concurrent compactions to control I/O impact. Compaction takes priority over queries — high compaction activity slows reads.
TimescaleDB chunk compression: TimescaleDB uses PostgreSQL native compression on chunks older than a configurable interval. Controlled by timescaledb.compression policy. Compressed chunks use the TIMESARDES format internally.
Observable misconfiguration signs: TSM files growing too large (>2GB default limit, causes "max points per timestamp exceeded" errors). WAL growing unbounded (compaction cannot keep up). Query latency spikes correlate with compaction activity (I/O contention).
The problem: Irregular sampling creates gaps in data. Some sensors report every second, others every hour. Some go offline for days and then resume. Queries that aggregate across time buckets return empty buckets for sensors with no data — causing jagged time series and confusing visualizations.
Fill strategies: Use gap filling functions. In InfluxDB: FILL() with options like linear interpolation, null, previous value, or a constant. In TimescaleDB: time_bucket_gapfill() combined with locf() (last observation carried forward) or interpolate().
Schema design for offline sensors: Store last known state as a separate measurement. Query last value plus current reading and merge in the application layer. This avoids null gaps in dashboards.
Query performance: Gap filling adds computation cost. For high-volume irregular data, precompute filled series during downsampling rather than filling at query time.
InfluxDB CQs: Execute on a schedule defined in the CQ definition (e.g., every 5 minutes, every 1 hour). The query runs against the current state of the database at execution time. If the CQ fails, it logs an error but continues running on the next schedule. CQs do not track state between executions — each run is independent.
TimescaleDB continuous aggregates: Implemented as materialized views with background refresh policies. TimescaleDB tracks what data is materialized and only recomputes delta changes since the last refresh. This is more efficient than full recomputation. Failure handling is baked into the refresh job — if it fails, it retries on next refresh window.
Resource consumption: InfluxDB CQs run as foreground queries during their schedule — they compete with user queries for resources. TimescaleDB continuous aggregates run as background jobs with configurable priority.
Failure visibility: InfluxDB CQs require monitoring logs for errors. TimescaleDB continuous aggregates have timescaledb_information.job_errors view for error visibility.
Architecture: Hub-and-spoke with eventual consistency. Each region has a local InfluxDB or TimescaleDB instance receiving sensor data. Local instances handle real-time queries and dashboards for that region. Data is replicated to a central aggregation cluster using the database's replication features (InfluxDB's Enterprise replication or TimescaleDB's logical replication to a read replica).
Cross-region queries: The central aggregation cluster receives replicated data and handles cross-region analytics. For InfluxDB, you would use Federation to query across instances. For TimescaleDB, read replicas can be set up in different regions and queried with foreign data wrappers or application-level aggregation.
Write path optimization: Sensor writes go to local instance only (< 10ms latency). Replication to central is async (eventual consistency, acceptable for analytics). If local instance goes offline, sensors buffer locally and replay when connection restores.
Conflict resolution: If you need strong consistency, use a distributed TSDB like QuestDB or TimescaleDB with Citus. Otherwise accept eventual consistency with timestamp-based conflict resolution (last write wins per point).
TimescaleDB approach: Use PostgreSQL row-level security (RLS). Add tenant_id as a column and create a partitioned hypertable by tenant_id alongside time. RLS policies filter data at the query level — CREATE POLICY tenant_isolation ON measurements FOR ALL USING (tenant_id = current_user).
Performance implications: RLS adds policy evaluation overhead on every query. For high-volume multi-tenant workloads, this can slow queries by 10-20%. Better approach: use separate hypertable partitions per tenant (TimescaleDB supports spatial partitioning via partitioning_param). Queries hitting a single tenant partition skip other tenant data entirely — no policy overhead.
InfluxDB approach: Use InfluxDB organizations and buckets. Each tenant gets an organization with quota limits. InfluxDB enforces isolation at the API level. Simpler but less flexible than PostgreSQL RLS.
Alternative for strict isolation: Separate database per tenant. Maximum isolation, maximum operational complexity. Only worth it for high-security tenants or small tenant counts (dozens, not thousands).
Write buffering options: Buffer in memory using a queue (in-process or external like Redis or Kafka). If the TSDB is overloaded and cannot accept writes, the buffer accepts new data up to its capacity. When the TSDB recovers, the buffer drains into the database.
Backpressure signals: Monitor write acknowledgment latency. If writes start queuing in the TSDB WAL (observable via WAL growth in InfluxDB), you are approaching capacity. Set write latency SLOs and trigger backpressure when p99 exceeds threshold.
Preventing data loss: The WAL is your safety net. In InfluxDB, data is acknowledged after the WAL persists, not after TSM write — so as long as WAL is growing (data being persisted), you have durability. If WAL growth slows or stalls, you are losing data. Use show stats to monitor writeBytesOK and pointsWrittenOK.
Consumer lag monitoring: If using Kafka as a buffer between sensors and TSDB, monitor consumer lag. If Kafka consumer falls behind producers, you are building up backlog. Alert on consumer lag exceeding defined thresholds.
QuestDB advantages for tick data: Columnar storage optimized for analytical queries on large datasets. Uses vectorized execution for aggregations. SQL compatibility means easier integration with existing BI tools. Designed for Kafka ingestion with native Kafka support. Handles 100k+ pts/s with sub-millisecond latency on aggregated queries.
InfluxDB advantages: Mature ecosystem with Telegraf for collection, Grafana for visualization, Flux for complex data transformation. Native retention policies and continuous queries. Better for pure time-series workloads with simpler query patterns.
Decision factors: If your queries are primarily aggregations over time windows across millions of rows (typical for financial analytics), QuestDB's columnar engine will outperform InfluxDB's TSM. If you have complex per-point transformations or need the InfluxDB ecosystem, InfluxDB wins.
Operational maturity: QuestDB is younger and less battle-tested at massive scale compared to InfluxDB. If you need 24/7 enterprise support and proven production deployments at scale, InfluxDB has the track record.
Further Reading
For related reading, see NoSQL Databases (coming soon) to understand other NoSQL options, and Database Scaling (coming soon) to learn about scaling strategies for high-volume data.
If you are building pipelines that feed TSDBs, Apache Kafka handles event streaming, and Apache Flink does stateful stream processing — useful for pre-aggregating data before it lands in your time-series store.
Conclusion
Key Points:
- Time-series databases are purpose-built for timestamped data with time-range query patterns
- InfluxDB excels at high write throughput with native downsampling
- TimescaleDB extends PostgreSQL with time-series capabilities for teams familiar with SQL
- Retention policies automate data lifecycle management
- Downsampling preserves query performance as data ages
- Tag cardinality is the most critical schema design decision — keep tag cardinality low
Quick Recap Checklist:
Before deploying a time-series database to production, verify each of the following:
- Retention policies defined and tested — confirmed data actually deletes at retention boundary
- Downsampling strategy planned — raw data for recent windows, aggregated data for long-range queries
- Tag cardinality analyzed — unique tag values bounded to thousands, not millions
- Bulk write batching configured — groups of 1000-5000 points per batch
- Timezone normalized to UTC — no local time zones stored in timestamps
- Write throughput baseline measured — know your points/second ceiling before hitting limits
- Compaction monitoring enabled — track compaction backlog and I/O impact
- Query timeout configured — prevent wide-range queries from timing out dashboards
- Continuous query / materialized view refresh monitored — alerting on failure or lag
- Multi-tenant isolation tested (if applicable) — RLS policies or bucket isolation verified
The lesson: time-series databases handle ingestion efficiently, but query performance over long ranges requires upfront planning of your downsampling strategy. The best time-series database cannot save a query pattern that requires scanning too much uncompressed data. Design your retention and downsampling around your actual query patterns, not just your data volume.
Category
Related Posts
DuckDB: The SQLite for Analytical Workloads
Explore DuckDB, the in-process analytical database that runs anywhere, handles columnar storage efficiently, and brings analytics to where your data lives.
Joins and Aggregations: SQL Patterns for Data Analysis
Master SQL joins and aggregation techniques for building efficient analytical queries in data warehouses and analytical databases.
Column-Family Databases: Cassandra and HBase Architecture
Cassandra and HBase data storage explained. Learn partition key design, column families, time-series modeling, and consistency tradeoffs.