QuestDB vs InfluxDB
Short version: pick QuestDB if your team writes SQL and wants existing PostgreSQL tooling to keep working, because SAMPLE BY 15m is one clause and any pg client connects. Pick InfluxDB if you are building a monitoring stack around Telegraf and want retention expiry handled for you per database. The query-language gap that used to define this comparison has narrowed, because InfluxDB 3 now speaks SQL and InfluxQL rather than Flux.
Your app is generating timestamped data and you need a database that's built for it. Maybe it's IoT sensor readings coming in every few seconds. Maybe it's server metrics, financial ticks, or user analytics events. PostgreSQL can store timestamps, sure, but once you're doing time-bucketed aggregations over millions of rows, you want something purpose-built.
QuestDB and InfluxDB both target this space, and they have historically wanted you to work with your data in very different ways. QuestDB extends SQL with time-series primitives like SAMPLE BY, so your team's existing SQL knowledge transfers directly. InfluxDB brings its own data model of measurements, tags, and fields, and for years it also brought its own query language.
A version note, updated 2026-08-26. InfluxDB 3 Core and Enterprise have been generally available since April 2025, and they query with SQL and InfluxQL. Flux, the functional pipeline language that InfluxDB 2 was built around, is now in maintenance mode: InfluxData's own docs state that "Flux is in maintenance mode and is not supported in InfluxDB 3" and recommend InfluxQL or SQL to future-proof new code. Sources: the InfluxDB 3 docs and the future of Flux.
The InfluxDB half of this post was rewritten for version 3 and every command in it was run against InfluxDB 3.10.5, which is what the Layerbase CLI and Layerbase Cloud provision today. If you're on a 2.x instance, the writes below still work but the queries don't: version 2 wants Flux, and version 3 doesn't have it.
Below, we'll run the same sensor pipeline in both so you can see exactly how different the developer experience is.
Contents
- Quick Comparison
- Set Up Both Databases with the Layerbase CLI
- The Same Task in Both
- Key Differences
- When to Pick QuestDB
- When to Pick InfluxDB
- FAQ
- Run Both on Layerbase Cloud
Quick Comparison
| QuestDB | InfluxDB | |
|---|---|---|
| Query language | SQL with extensions (SAMPLE BY, LATEST ON) | SQL and InfluxQL in v3; Flux in v2, now maintenance-only |
| Data model | Tables, columns, rows | Measurements, tags, fields |
| Wire protocol | PostgreSQL (port 8812) | HTTP API, plus Arrow Flight over gRPC for queries (port 8086) |
| Write format | SQL INSERT or InfluxDB Line Protocol | InfluxDB Line Protocol or Point builder |
| Client library | Any PostgreSQL client (pg, psycopg2) | Official SDK (@influxdata/influxdb3-client) |
| Retention | Manual (DROP PARTITION) or detach | Built-in per-database retention period |
| Learning curve | Low if you know SQL | Low on the read side, new vocabulary on the write side |
| Sweet spot | Teams that want SQL, high-throughput ingestion | Telegraf/Grafana ecosystem, pipeline-style queries |
Set Up Both Databases with the Layerbase CLI
We'll run both locally with the Layerbase CLI (formerly SpinDB). No Docker, no manual config. (What is the Layerbase CLI?)
Install the CLI globally:
npm i -g layerbase # npm
pnpm add -g layerbase # pnpmCreate and start both instances:
lbase create quest1 -e questdb --start
lbase create influx1 -e influxdb --startCheck their URLs:
lbase url quest1postgresql://localhost:8812/qdblbase url influx1http://127.0.0.1:8086QuestDB gives you a PostgreSQL connection string. InfluxDB gives you an HTTP URL. That tells you a lot about what comes next.
The CLI creates a database named after the container, so influx1 is both the instance and the database you write into. There is no token locally: it starts InfluxDB 3 Core with authentication off. Version 2 asked for an organization, a bucket, and an API token before the first write; version 3 dropped organizations, renamed buckets to databases, and creates one on first write.
Confirm what you got:
curl -s http://127.0.0.1:8086/ping{
"product_name": "InfluxDB 3 Core",
"version": "3.10.5",
"revision": "df21654049",
"process_id": "ccc158ef-2e48-4df7-9d45-605a7508108d"
}The Same Task in Both
The task: insert 288 sensor readings (3 sensors, 96 readings each, spanning 24 hours), then query average temperature per 15-minute bucket. Same data, same question, very different implementations.
Set up a project with both client libraries:
mkdir tsdb-compare && cd tsdb-compare
pnpm init
pnpm add pg @influxdata/influxdb3-client
pnpm add -D tsx typescript @types/pgBoth scripts use the same generated data:
type Reading = {
sensorId: string
temperature: number
humidity: number
ts: Date
}
function generateReadings(): Reading[] {
const sensors = ['sensor_a', 'sensor_b', 'sensor_c']
const readings: Reading[] = []
const now = new Date()
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
for (const sensorId of sensors) {
let baseTemp = 20 + Math.random() * 10
let baseHumidity = 40 + Math.random() * 20
for (let i = 0; i < 96; i++) {
const ts = new Date(
twentyFourHoursAgo.getTime() +
i * 15 * 60 * 1000 +
Math.random() * 60 * 1000,
)
baseTemp += (Math.random() - 0.5) * 2
baseHumidity += (Math.random() - 0.5) * 3
readings.push({
sensorId,
temperature: Math.round(baseTemp * 100) / 100,
humidity:
Math.round(Math.max(0, Math.min(100, baseHumidity)) * 100) / 100,
ts,
})
}
}
return readings.sort((a, b) => a.ts.getTime() - b.ts.getTime())
}
const readings = generateReadings()Now let's see how each database handles it differently.
QuestDB: SQL All the Way
Create a file called questdb-sensors.ts:
import pg from 'pg'
// ... paste generateReadings() above ...
const client = new pg.Client({
host: 'localhost',
port: 8812,
database: 'qdb',
})
await client.connect()
console.log('Connected to QuestDB')Create the table. QuestDB's SYMBOL type interns string values for fast filtering, and timestamp(ts) designates the time column:
await client.query(`
CREATE TABLE IF NOT EXISTS sensors (
sensor_id SYMBOL,
temperature DOUBLE,
humidity DOUBLE,
ts TIMESTAMP
) timestamp(ts) PARTITION BY HOUR WAL;
`)Insert the data with parameterized SQL:
for (const r of readings) {
await client.query(
'INSERT INTO sensors (sensor_id, temperature, humidity, ts) VALUES ($1, $2, $3, $4)',
[r.sensorId, r.temperature, r.humidity, r.ts],
)
}
console.log(`Inserted ${readings.length} rows`)Query average temperature per 15-minute bucket:
const sampled = await client.query(`
SELECT
sensor_id,
avg(temperature) as avg_temp
FROM sensors
SAMPLE BY 15m
ALIGN TO CALENDAR
`)
console.log('\n15-minute averages (first 10 buckets):')
console.log('sensor_id | avg_temp')
console.log('-----------|----------')
for (const row of sampled.rows.slice(0, 10)) {
console.log(
`${row.sensor_id.padEnd(10)} | ${Number(row.avg_temp).toFixed(2).padStart(8)}`,
)
}
console.log(`... (${sampled.rows.length} total buckets)`)
await client.end()15-minute averages (first 10 buckets):
sensor_id | avg_temp
-----------|----------
sensor_a | 23.41
sensor_b | 27.08
sensor_c | 21.83
sensor_a | 24.12
sensor_b | 26.55
sensor_c | 22.31
sensor_a | 23.78
sensor_b | 27.44
sensor_c | 22.09
sensor_a | 24.63
... (288 total buckets)That's the whole thing. SAMPLE BY 15m. If you can write a SELECT, you can write a QuestDB time-bucketed aggregation.
InfluxDB: Points, Tags, and SQL
Create a file called influxdb-sensors.ts:
import { InfluxDBClient, Point } from '@influxdata/influxdb3-client'
// ... paste generateReadings() above ...
const INFLUX_HOST = 'http://127.0.0.1:8086'
const INFLUX_DATABASE = 'influx1'
const INFLUX_TOKEN = ''
const client = new InfluxDBClient({
host: INFLUX_HOST,
token: INFLUX_TOKEN,
database: INFLUX_DATABASE,
})
console.log('Connected to InfluxDB')Write the data. No INSERT statements here. You build Point objects and classify each value as a tag (indexed, for filtering) or a field (not indexed, for aggregation):
const points = readings.map((r) =>
Point.measurement('sensors')
.setTag('sensor_id', r.sensorId)
.setFloatField('temperature', r.temperature)
.setFloatField('humidity', r.humidity)
.setTimestamp(r.ts),
)
await client.write(points, INFLUX_DATABASE, undefined, { precision: 'ms' })
console.log(`Wrote ${points.length} points to InfluxDB`)Query average temperature per 15-minute bucket. On version 3 that's SQL, and the windowing function is date_bin:
const sql = `
SELECT
date_bin(INTERVAL '15 minutes', time) AS bucket,
sensor_id,
avg(temperature) AS avg_temp
FROM sensors
WHERE time > now() - INTERVAL '24 hours'
GROUP BY bucket, sensor_id
ORDER BY bucket, sensor_id
`
const rows: Record<string, unknown>[] = []
for await (const row of client.query(sql, INFLUX_DATABASE)) {
rows.push(row)
}
console.log('\n15-minute averages (first 10 buckets):')
console.log('sensor_id | avg_temp')
console.log('-----------|----------')
for (const row of rows.slice(0, 10)) {
const sensorId = String(row.sensor_id).padEnd(10)
const avgTemp = Number(row.avg_temp).toFixed(2).padStart(8)
console.log(`${sensorId} | ${avgTemp}`)
}
console.log(`... (${rows.length} total buckets)`)
await client.close()Connected to InfluxDB
Wrote 288 points to InfluxDB
15-minute averages (first 10 buckets):
sensor_id | avg_temp
-----------|----------
sensor_a | 23.76
sensor_b | 29.96
sensor_c | 25.93
sensor_a | 23.94
sensor_b | 29.25
sensor_c | 25.75
sensor_a | 24.85
sensor_b | 28.40
sensor_c | 26.51
sensor_a | 25.55
... (288 total buckets)Same shape of answer, different path to it. The readings are randomly generated per run, so the temperatures won't match the QuestDB script's, but the 288 buckets and the per-sensor breakdown line up exactly.
The read side is now familiar: it's a SELECT with a GROUP BY. The write side is where the vocabulary is new. You still have to decide what's a measurement, what's a tag, and what's a field, and that decision is permanent, because tags are indexed and fields are not.
If you're porting queries from a 1.x instance, InfluxQL is still supported and the same client runs it with one option:
const influxQl = `
SELECT mean(temperature)
FROM sensors
WHERE time > now() - 24h
GROUP BY time(15m), sensor_id
`
for await (const row of client.query(influxQl, INFLUX_DATABASE, {
type: 'influxql',
})) {
console.log(JSON.stringify(row))
}{"iox::measurement":"sensors","time":1787637600000,"sensor_id":"sensor_a","mean":23.94}
{"iox::measurement":"sensors","time":1787638500000,"sensor_id":"sensor_a","mean":24.85}
{"iox::measurement":"sensors","time":1787639400000,"sensor_id":"sensor_a","mean":25.55}
{"iox::measurement":"sensors","time":1787640300000,"sensor_id":"sensor_a","mean":26.52}GROUP BY time(15m) is the InfluxQL spelling of date_bin. It emits one series per tag combination and includes empty windows with no mean, where the SQL path returns a flat result set and skips them.
Key Differences
Two Dialects of SQL
This used to be the biggest decision point, and version 3 shrank it to a matter of dialect. QuestDB is SQL with a handful of extensions. If your team writes SQL every day, QuestDB is immediately productive. SAMPLE BY 15m reads like pseudocode.
InfluxDB used to answer Flux, a functional pipeline language that every developer on the team had to learn. InfluxData retired that bet: Flux is in maintenance mode and InfluxDB 3 dropped it for SQL and InfluxQL. So both engines now take a SELECT, and the difference is how each one expresses a time bucket.
Same query, side by side:
QuestDB:
SELECT sensor_id, avg(temperature) FROM sensors SAMPLE BY 15m ALIGN TO CALENDARInfluxDB 3:
SELECT date_bin(INTERVAL '15 minutes', time) AS bucket, sensor_id, avg(temperature)
FROM sensors
WHERE time > now() - INTERVAL '24 hours'
GROUP BY bucket, sensor_idQuestDB's clause is shorter and the bucketing is implicit in the designated timestamp. InfluxDB's is a standard DataFusion GROUP BY on a computed column, which is more typing but also more ordinary: anything else you know about SQL aggregation transfers.
For a team coming from Postgres, the remaining gap is not the query language. It's the write model, where InfluxDB still wants you to sort every value into a tag or a field before the first point lands.
For reference, this is the version 2 shape of that same aggregation, which no longer runs on version 3 at all:
from(bucket: "default")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "sensors")
|> filter(fn: (r) => r._field == "temperature")
|> aggregateWindow(every: 15m, fn: mean, createEmpty: false)PostgreSQL Wire Protocol vs HTTP API
QuestDB speaks the PostgreSQL wire protocol. Connect with pg, psycopg2, JDBC, or any other PG client. Your existing database tooling, ORMs, connection poolers, and monitoring tools work out of the box.
InfluxDB exposes an HTTP API. Writes are line protocol over POST /api/v3/write_lp, and reads go through the official SDK (@influxdata/influxdb3-client for JavaScript, influxdb3-python for Python) or the plain HTTP query endpoints /api/v3/query_sql and /api/v3/query_influxql. Fine for applications, but psql, TablePlus, and DBeaver don't work directly.
Worth knowing: the version 3 SDK sends queries over Arrow Flight, which is gRPC. That's fast in an application and awkward in a serverless function, which is why the JSON-over-HTTP query endpoints exist as an escape hatch.
For quick ad-hoc queries, QuestDB's PG protocol is a real advantage. lbase connect quest1 and start running SQL immediately. InfluxDB 3 Core has no built-in web console the way version 2 did, so either use lbase query influx1 "SELECT ...", curl the query endpoint, or run queries from the Layerbase Cloud console.
Data Retention
InfluxDB has retention built into the database. Set how long data lives when you create it, or change it later, and old data gets deleted automatically:
influxdb3 create database --host http://127.0.0.1:8086 --retention-period 7d metrics_raw
influxdb3 update database --host http://127.0.0.1:8086 --database influx1 --retention-period 30dDownsampling is the part that got worse in version 3, and it's worth being straight about. Version 2 had Flux tasks: a scheduled query with |> to(bucket: ...) on the end that rolled raw points into summaries automatically. Version 3 has no Flux and therefore no Flux tasks. It ships a Processing Engine that runs Python plugins on a schedule inside the server, but that only exists when the server was started with a plugin directory configured. The portable answer on any version 3 instance, managed or local, is to run the rollup from your own code: SELECT the aggregate, write the result back as points into a second database with a longer retention period. It works, it's about fifteen lines, and it is more assembly than version 2 asked for.
QuestDB manages retention manually. You detach or drop partitions to remove old data. No built-in "delete everything older than 30 days" setting. Some teams hate this. Others prefer the explicit control.
If automatic data lifecycle management matters to you, InfluxDB handles it natively. If you want full control over what gets deleted and when, QuestDB gives you that.
Ecosystem
InfluxDB has a larger ecosystem for monitoring and observability. Telegraf is a widely-deployed metrics collection agent with hundreds of input plugins. The TIG stack (Telegraf + InfluxDB + Grafana) is a well-established pattern for infrastructure monitoring.
QuestDB is leaner. It focuses on being a fast, SQL-compatible time-series database. It supports the InfluxDB Line Protocol for ingestion (so Telegraf can write to QuestDB too), has a built-in web console, and works with Grafana via its PostgreSQL-compatible interface. But the surrounding ecosystem is smaller.
When to Pick QuestDB
Pick QuestDB if:
- Your team already knows SQL. Strongest argument. Zero new query language to learn.
SAMPLE BYandLATEST ONare intuitive extensions, not a paradigm shift. - You want existing PostgreSQL tooling. Connection poolers, ORMs, CLI tools, TablePlus, DBeaver, monitoring dashboards that speak PG wire protocol. All of it just works.
- Your queries are time-bucketed aggregations and "latest value" lookups. These two patterns cover the majority of time-series use cases, and QuestDB handles both in one clause each.
- You value simplicity. QuestDB does time-series storage and querying. It doesn't try to be a metrics platform, a dashboarding tool, or a task scheduler.
When to Pick InfluxDB
Pick InfluxDB if:
- You need the Telegraf ecosystem. Collecting metrics from dozens of sources (servers, containers, cloud services, network devices)? Telegraf's plugin library is unmatched. InfluxDB is its native backend.
- Built-in retention matters. Storing high-frequency data that needs automatic expiry? A retention period on the database handles it without external tooling. Downsampling on top of that is your own scheduled job on version 3.
- The measurement, tag, and field data model fits your data. Tags are indexed, fields are not, and if your metrics decompose cleanly along those lines the model does useful work for you. (If you were picking InfluxDB specifically for Flux's
|>pipeline style, note that version 3 does not support Flux at all.) - You're building a monitoring stack. The TIG stack is battle-tested for infrastructure monitoring. If that's your use case, you're swimming with the current.
FAQ
Should I use QuestDB or InfluxDB?
QuestDB if your team writes SQL and you want your existing PostgreSQL clients, ORMs, and GUI tools to connect without a new driver. InfluxDB if you are assembling a monitoring stack, want Telegraf's plugin library feeding it, and want data expiry configured on the database instead of managed by hand. Neither is a bad answer for general time-series storage, which is why the tiebreaker is usually your team and your tooling rather than the database.
Does InfluxDB still use Flux?
Not in version 3. InfluxDB 3 queries with SQL and InfluxQL, and InfluxData's docs say plainly that Flux is in maintenance mode and unsupported in version 3, recommending InfluxQL or SQL for new code. The walkthrough in this post is version 3 SQL, tested on 3.10.5. Flux still runs on a 2.x instance, but writing new Flux in 2026 means writing against a language its maintainers have stopped developing, and the version 2 query endpoint is simply not there on version 3: POST /api/v2/query returns a 404 while POST /api/v2/write still works.
Can I connect to QuestDB with a normal Postgres client?
Yes, and it is one of the better reasons to pick it. QuestDB speaks the PostgreSQL wire protocol on port 8812, so pg, psycopg2, JDBC, psql, TablePlus, and DBeaver all connect with no special driver. InfluxDB exposes an HTTP API instead, so you use its SDK, the Layerbase query console, lbase query, or raw HTTP.
Which one handles data retention better?
InfluxDB, though by a smaller margin than it used to be. Retention is part of the database, so you set how long data lives with influxdb3 create database --retention-period 7d and old data expires on its own. Downsampling before it goes is no longer free: version 2 did that with Flux tasks, and on version 3 you run the rollup yourself or configure the Processing Engine. QuestDB expects you to detach or drop partitions yourself. Some teams find that tedious and some prefer knowing exactly what gets deleted and when.
Can QuestDB ingest InfluxDB line protocol?
Yes. QuestDB supports the InfluxDB Line Protocol for ingestion, which means Telegraf can write into QuestDB and you can keep an existing collection pipeline while changing the database underneath it. That takes a lot of the sting out of the ecosystem gap, though the broader TIG-stack tooling and documentation still assume InfluxDB.
Is QuestDB Cloud self-serve?
No. As of 2026-08-25, questdb.com/cloud and questdb.com/pricing route you to an enterprise trial or a "contact us" demo, with a separate bring-your-own-cloud path behind the same sales conversation. There is no sign-up-and-pay flow. If you want managed QuestDB without talking to a salesperson, Layerbase Cloud provisions it directly; managed QuestDB in 2026 goes through the options in more detail.
Run Both on Layerbase Cloud
Want to skip local setup entirely? Layerbase Cloud provisions either engine. Pick QuestDB or InfluxDB on the create page and grab your connection details from the Quick Connect panel.
To manage your local instances:
lbase stop quest1 # Stop QuestDB
lbase stop influx1 # Stop InfluxDB
lbase start quest1 # Start QuestDB
lbase start influx1 # Start InfluxDB
lbase list # See all your database instancesThe Layerbase CLI runs 20+ database engines from one binary. Running QuestDB and InfluxDB side by side is the fastest way to decide which one fits. If you prefer a GUI, Layerbase Desktop is available for macOS.
Keep reading
- The InfluxDB 3 Core 72-hour query limit: what it is, why it exists, and your two ways outInfluxDB 3 Core refuses queries that touch more than 432 Parquet files, which at the default 10-minute file duration works out to about 72 hours of data per query. The data is still there. Here is the mechanism, the knob, what Layerbase actually runs, and the two honest exits.
- Branching time-series databasesNobody branches a time-series database. Neon branches Postgres, PlanetScale branches MySQL, and telemetry data gets a stale CSV sample. QuestDB and InfluxDB branch on Layerbase now, and it changes how you test against real metrics.
- Best Database for IoTA practical guide to picking the right database for IoT sensor data, covering QuestDB, InfluxDB, and PostgreSQL with SpinDB examples you can run locally.
- What Is a Time-Series Database?A practical guide to understanding when your timestamped data needs a dedicated time-series database and when PostgreSQL is enough.