Skip to content

QuestDB vs InfluxDB

12 min readQuestDBInfluxDBTime Series

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

QuestDBInfluxDB
Query languageSQL with extensions (SAMPLE BY, LATEST ON)SQL and InfluxQL in v3; Flux in v2, now maintenance-only
Data modelTables, columns, rowsMeasurements, tags, fields
Wire protocolPostgreSQL (port 8812)HTTP API, plus Arrow Flight over gRPC for queries (port 8086)
Write formatSQL INSERT or InfluxDB Line ProtocolInfluxDB Line Protocol or Point builder
Client libraryAny PostgreSQL client (pg, psycopg2)Official SDK (@influxdata/influxdb3-client)
RetentionManual (DROP PARTITION) or detachBuilt-in per-database retention period
Learning curveLow if you know SQLLow on the read side, new vocabulary on the write side
Sweet spotTeams that want SQL, high-throughput ingestionTelegraf/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:

bash
npm i -g layerbase    # npm
pnpm add -g layerbase # pnpm

Create and start both instances:

bash
lbase create quest1 -e questdb --start
lbase create influx1 -e influxdb --start

Check their URLs:

bash
lbase url quest1
text
postgresql://localhost:8812/qdb
bash
lbase url influx1
text
http://127.0.0.1:8086

QuestDB 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:

bash
curl -s http://127.0.0.1:8086/ping
json
{
  "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:

bash
mkdir tsdb-compare && cd tsdb-compare
pnpm init
pnpm add pg @influxdata/influxdb3-client
pnpm add -D tsx typescript @types/pg

Both scripts use the same generated data:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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()
text
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:

typescript
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):

typescript
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:

typescript
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()
text
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:

typescript
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))
}
text
{"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:

sql
SELECT sensor_id, avg(temperature) FROM sensors SAMPLE BY 15m ALIGN TO CALENDAR

InfluxDB 3:

sql
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_id

QuestDB'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:

text
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:

bash
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 30d

Downsampling 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 BY and LATEST ON are 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:

bash
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 instances

The 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.