Skip to main content
Version: 1.0.8

QuestDB

Writes time series at volume, or queries them back. Use it for series you generate yourself — a calculated OEE, a derived rate — that deserve a trend but are not tags.

Type: questdb · Category: Datasource · Ports: one input · one output

The QuestDB node as it appears on the pipeline canvas.The QuestDB node as it appears on the pipeline canvas.

When to use it​

  • Storing a calculated value you want to chart over time
  • High-rate writes that would overwhelm a relational database
  • Querying history back for a report
Reach for something else when

For tag history, enable history on the tag instead — the historian already does this, correctly, with rollups and retention.

Settings​

SettingKeyDefaultPurpose
Modemodeingressingress (high-rate writes) or query.
ConnectionconnectionId—Which QuestDB connection.
Tabletable—Ingestion target.
Symbolssymbols—Which fields are indexed labels rather than values.
Queryquery—For query mode.
Timeout / retriestimeout, maxRetries, retryDelay0Per-write limits.

The payload is one object, an array of objects, or the same as JSON text. Each object becomes one row: the fields named under Symbols become symbols, every other field a column. A null field is left out of the row, and a nested object or array is stored as JSON text. A payload that cannot be turned into rows fails the node at once rather than being buffered for a replay that would only fail again.

When each row happened New in 1.0.8​

Ingress mode has a Timestamp section that decides each row's designated timestamp — the time QuestDB files the row under.

SettingKeyDefaultPurpose
Timestamp fieldtimestampField—The payload field holding when the row was produced, e.g. ts. Empty: use the message time.
Timestamp formattimestampFormatautoAuto-detect, ISO 8601 text, or Unix seconds / milliseconds / microseconds / nanoseconds.
Timezone for text without an offsettimestampTimezoneUTCApplies to ISO text that carries no zone.
Also store the field as a columnkeepTimestampFieldoffOff: the value becomes the row's timestamp and is not written twice.
Use message time as fallbackuseMessageTimestamponWhen a row has no usable timestamp field, use the time the message entered the pipeline.

A row's time is, in order: the timestamp field, if it is readable and plausible; else the message time, if the fallback is on; else the moment the node wrote it. Plausible means after 1 January 2000 and no more than 10 minutes ahead of the gateway's clock — a device with a dead clock battery does not get to file rows in 1970 or next week.

Rows carry the time they happened, not the time QuestDB received them

1.0.8 Before 1.0.8 the node let QuestDB stamp every row with its own arrival time. A backlog that store-and-forward had held through an outage therefore landed at the moment it drained — every row wrong by the length of the outage. Now the time is decided once, before the write, and a buffered row replays at exactly the time it would have had live, with the same column types (a whole number stays an integer column; it used to come back as a float). A dashboard that grouped QuestDB rows by arrival time will see them move to when they happened.

Stamping the time on the gateway also makes a resend recognisable: on a table with DEDUP UPSERT KEYS including the timestamp, the same batch written twice stays one set of rows.

How the node reaches QuestDB Changed in 1.0.8​

The node and the historian now pick the ILP transport by one rule, taken from the connection: an explicit transport on the connection wins; otherwise a connection whose ILP port is 9009 keeps TCP; anything else uses HTTP (port 9000 unless the connection names another). HTTP is the transport QuestDB recommends, because it reports rejected rows and retries a failed batch.

A connection with an empty or custom ILP port now writes over HTTP

Before 1.0.8 this node used HTTP only when the ILP port was exactly 9000 and TCP otherwise — the reverse of the historian, so one connection could be written over two transports. A connection that left the ILP port empty, or named a port other than 9000 or 9009, now writes over HTTP. If that port is a TCP listener, set the port to 9009 or the transport to TCP on the connection.

Example — writing a derived series​

OEE is calculated, not measured, so it is not a tag — but you still want to trend it.

Mode: ingress
Table: oee_by_line
Symbols: line, shift
Timestamp field: timestamp
Timestamp format: Unix milliseconds

The row is filed at timestamp — the time the figure describes — rather than at the moment the pipeline happened to compute it. 1.0.8

In

{
"payload": {
"line": "LINE1",
"shift": "A",
"availability": 0.92,
"performance": 0.95,
"quality": 0.97,
"oee": 0.847,
"timestamp": 1786454700000
}
}

What you can reference

PathResolves to
{{payload}}The whole payload
{{payload.units}}A field, at any depth — {{payload.order.line}} works
{{topic}}The message topic
{{timestamp}} · {{id}}The message timestamp and id
{{vars.shift}}Message metadata, also available as {{metadata.shift}}
{{origin.id}} · {{origin.type}}The node that produced the message

With no upstream message the query runs exactly as typed, with nothing substituted — which is what makes a plain SELECT work behind an Inject.

A path that does not exist is left as literal text

{{payload.data}} against a message with no data field is not blanked and not bound — the token stays in the statement exactly as you typed it and is handed to the database as raw SQL:

-- you wrote
WHERE units = {{payload.units}}
WHERE data = {{payload.data}}

-- the database receives
WHERE units = ? -- bound, value 42
WHERE data = {{payload.data}} -- raw text, no argument

Which is a syntax error, and the error the database returns names the {{, not the missing field. The rule is deliberate — a silent NULL would make a WHERE match the wrong rows and a DELETE match too many — but it means a typo in a path fails at the database, not in the editor. Check the path against a real message in the Debug node before you rely on it.

Note this is the opposite of what the notification and AI nodes do with the same syntax, where an unresolved token becomes empty.

Gotchas​

  • Mark labels as symbols, never values. A symbol is stored as a repeated label and is what you filter and group by. Making a continuously varying number a symbol builds an enormous index for nothing, and it is painful to undo once the table has data in it.

See also​