A fleet of ten devices reporting every minute fits comfortably in an ordinary PostgreSQL table. A fleet of a thousand devices reporting every ten seconds does not, at least not without some thought. Somewhere between those two points, a decision has to be made about where telemetry lives, and it is one of the few architecture choices in an IoT project that is genuinely difficult to reverse once a few hundred million rows have accumulated. This is about how to make that call before it makes itself.
Why telemetry does not behave like other data
Most business data is read far more often than it is written, and individual rows get updated as circumstances change. Telemetry is the opposite. It is written constantly, almost never updated, and read in aggregate rather than row by row: an average temperature over the last hour, the maximum vibration reading over a shift, a count of offline events per device per day. Nobody queries a single sensor reading from three weeks ago in isolation; they query a window and a function.
That write-heavy, append-only, aggregate-read pattern is exactly what time-series databases are built around, and exactly what standard relational indexing handles badly at volume. A B-tree index on a timestamp column keeps growing, insert throughput degrades as the table grows, and a query that scans a month of readings for one device starts doing far more I/O than it should. None of this shows up in a proof of concept with a handful of devices. It shows up eight months later, when the fleet has grown and the dashboard that used to load in half a second now takes twelve.
The case for staying on plain PostgreSQL
Most IoT projects already have PostgreSQL somewhere in the stack, holding devices, users, sites, and configuration. Adding a second database purely for telemetry means a second thing to provision, back up, monitor, patch and eventually explain to whoever inherits the system. That cost is real and it does not show up on an architecture diagram.
PostgreSQL can be pushed a long way before it becomes the bottleneck, provided the table is partitioned by time from the start rather than retrofitted later. Native declarative partitioning, one partition per day or week, keeps each partition small enough that indexes stay efficient and old partitions can be dropped wholesale for retention instead of deleted row by row, which is far cheaper. Combined with BRIN indexes on the timestamp column, which are far smaller than B-tree indexes and suit naturally ordered, append-only data well, a partitioned Postgres table will comfortably handle a moderate fleet reporting every few seconds.
Where plain Postgres stops being enough
The limits tend to show up around three things: sustained write throughput once the fleet reaches the low thousands of devices reporting at high frequency, the cost of computing rollups and downsampled aggregates on the fly rather than having them maintained automatically, and storage growth once nobody is actively deleting old data because someone might need it for a warranty dispute in eighteen months. None of these are hard failures. They are gradual degradation, which is worse in some ways, because there is rarely a single moment that forces the conversation.
What a dedicated time-series database actually buys
TimescaleDB, an extension that runs inside PostgreSQL itself, is usually the first stop, precisely because it does not require a second database engine. It automates the partitioning that would otherwise be built by hand, adds continuous aggregates that keep hourly and daily rollups up to date incrementally rather than recomputing them per query, and adds native compression for older chunks that can shrink storage substantially once data is no longer being actively written to. For a team already committed to PostgreSQL, this is the smallest step that solves the largest share of the problem.
A fully separate engine, InfluxDB being the most established example, goes further: purpose-built storage for time-stamped data, very high ingest rates even under bursty, out-of-order writes from devices with unreliable connectivity, and a query language built around windows and aggregation rather than joins. The trade-off is that it is a genuinely different system, with its own operational model, its own backup strategy, and its own learning curve for whoever is on call. It also means telemetry and relational data live apart, so a dashboard that needs to show the last reading joined against device metadata and the account it belongs to has to do that join in application code rather than in a single query.
The variables that actually decide it
Ignore the marketing comparisons between individual products for a moment. The decision comes down to a small number of project-specific facts:
- Ingest volume: rows per second across the whole fleet at the busiest point, not the average, and where that number is expected to be in two years, not today.
- Whether telemetry needs to be joined against relational data in the same query, or whether it is consumed on its own through dashboards and alerts.
- Retention requirements: how long raw readings need to be kept versus how long downsampled aggregates are enough, and whether there is a regulatory or contractual reason for either.
- The team that will operate this in production, and whether taking on a second database engine is a reasonable ask of them, or a resourcing problem in disguise.
- How tolerant the system is of out-of-order or delayed writes, which matters a great deal for field devices on intermittent connectivity and much less for fixed installations on reliable networks.
Fleet size on its own is a poor proxy for any of this. A hundred devices reporting once a second can outpace ten thousand devices reporting once an hour, and the query pattern matters as much as the write volume.
A path that does not require betting early
The most defensible starting point for most projects is a partitioned PostgreSQL table from day one, even at small scale, because retrofitting partitioning onto a table that already has real production data in it is far more disruptive than designing it in from the start. That buys headroom without committing to any extra infrastructure. If growth or query patterns later justify it, TimescaleDB is a comparatively small migration from that position, since the underlying engine does not change. Moving to a fully separate engine such as InfluxDB is a bigger step and worth taking only once there is a specific, measured reason for it, rather than because a comparison article says it scales better.
This is the same reasoning we apply on our own IoT platform work: design the schema and partitioning as if the fleet will grow, but avoid taking on a second production database until the numbers say it is actually necessary.
Questions worth answering before committing
- What is the realistic peak write rate across the fleet in two years, not the number the pilot is running today?
- Do the main queries need to join telemetry with account, device or site data, or do they stand alone?
- Is raw data needed indefinitely, or is a downsampled rollup enough once data passes a certain age?
- Who operates this system day to day, and have they run a time-series database, or would this be their first?
- Is the table partitioned by time already, regardless of which engine sits underneath it?
If most of those answers point towards moderate volume, data that needs to sit alongside relational records, and a small team, plain partitioned PostgreSQL is very likely the right call. If the answers point towards high-frequency ingest from a large fleet, standalone analytics, and a team that can support another production system, the case for a dedicated time-series engine gets much stronger. Either way, the decision is best made deliberately, before the first partition-free table has a year of data in it.