Skip to content
Browse docs

QuestDB: ILP ingest

Every QuestDB database on Layerbase Cloud is reachable two ways: the PostgreSQL wire endpoint for SQL, and a dedicated HTTPS endpoint that speaks InfluxDB Line Protocol for high-throughput writes. This page covers where to find the ingest endpoint, how to write to it from the official clients, what it deliberately does not serve, and the limits and errors you should expect a pipeline to handle.

What you get

A Postgres wire endpoint for SQL. psql, the pg npm package, psycopg, pgx, and Grafana's Postgres data source all connect to it and run your SAMPLE BY and LATEST ON queries. See connecting to QuestDB for per-client snippets.

An ILP endpoint for ingest. Line protocol over HTTP, on its own hostname, on port 443, with a publicly trusted certificate. The hostname is your database hostname with -ilp appended to the first label, so a database at mydb-cool-river.cloud.layerbase.dev ingests at mydb-cool-river-ilp.cloud.layerbase.dev. It serves POST /write and POST /api/v2/write, honors query parameters such as precision, answers GET /ping with a 204, and answers GET /settings with the protocol-version negotiation the official clients perform on connect, so binary doubles and arrays work.

Authentication is HTTP Basic with the same username and password as the Postgres endpoint. There is no separate ingest token to mint or rotate.

Find your ILP endpoint

Open your QuestDB database in the dashboard and click Connect. The Parameters tab has an ILP ingest section with the ILP URL and a ready-to-paste config string, which carries your password and stays masked until you reveal it. The Snippets tab carries ILP ingest (Python) and ILP ingest (curl), already filled in with your hostname and credentials.

Over the API, GET /v1/databases/:id returns ilpUrl and ilpConfigString for a QuestDB database. Both are omitted until the ingest hostname's DNS record is live, which is usually under a minute after create, so a script that provisions and then writes should treat a missing ilpUrl as "not ready yet" rather than an error. In a stack, the same config string arrives in the environment block as QUESTDB_ILP_CONF.

Quick start

The official QuestDB Python client takes the config string as-is. Install it with pip install questdb.

Write a row with the QuestDB Python client
from questdb.ingress import Sender, TimestampNanos

conf = "https::addr=YOUR-DB-ilp.cloud.layerbase.dev:443;username=YOUR_USER;password=YOUR_PASSWORD;"

with Sender.from_conf(conf) as sender:
    sender.row(
        "sensors",
        symbols={"device": "d1"},
        columns={"temperature": 21.5},
        at=TimestampNanos.now(),
    )
    sender.flush()

Anything that can POST line protocol with Basic auth works too, so curl is a fine way to prove the endpoint before you wire up a client.

Write a row with curl
curl -u 'YOUR_USER:YOUR_PASSWORD' \
  --data-binary 'sensors,device=d1 temperature=21.5' \
  'https://YOUR-DB-ilp.cloud.layerbase.dev/write'

Tables are created on first write, so there is no schema step before either of these. A successful write returns 204 with no body.

Other clients

The config string is the standard QuestDB client configuration format, so it works unchanged in every official client: Python, Java, Rust, C and C++, Go, .NET, and Node.js. Point the client at the ILP host on port 443 over https and pass the username and password. QuestDB's ingestion overview documents each client.

Any HTTP line-protocol writer that supports Basic auth can write here, which includes collectors such as Telegraf through its InfluxDB output. We have not run a Telegraf pipeline against this endpoint end to end ourselves, so treat that path as untested by us rather than blessed.

What the ILP endpoint does not serve

The ingest hostname carries the write paths and nothing else. Anything outside them returns 404. Specifically:

  • Raw ILP over TCP (port 9009) is not exposed. If your pipeline is pinned to the TCP transport, point it at the HTTP transport instead; every official client supports both.
  • QuestDB's REST query, import, and export endpoints (/exec, /imp, /exp) are not exposed, and neither is the QuestDB web console. Queries go over the Postgres wire endpoint or the query console in the dashboard.
  • QWP, QuestDB's WebSocket protocol, is not exposed. It is newer surface area we have not wired through the connection layer.

Limits

  • Request body: 100 MiB. A larger body is refused with a 413. The official clients cap their own buffer at the same 100 MiB and flush long before it, at 75,000 rows or one second by default, so this ceiling is really a guard against a hand-rolled writer batching without bound.
  • gzip is accepted. Send Content-Encoding: gzip and the body is decompressed before parsing.
  • Request rate: 3,000 requests per minute, per database, per source address. Over that you get a 429 with a Retry-After. A client that batches at the defaults above sends nowhere near this.
  • Request timeout: 120 seconds.

Errors

The statuses a pipeline should expect to see, and what each one means:

StatusMeaningWhat to do
204The rows were accepted.Nothing to do. Line protocol returns no body on success.
400The line protocol was malformed.The response carries the QuestDB error itself, and the official clients name the offending line.
401The credentials were missing or wrong.Use the same username and password as the Postgres endpoint.
402The account is paused.Settle the outstanding invoice in billing and writes resume.
413The request body was larger than 100 MiB.Flush more often. The official clients flush well below this on their own.
423The database is locked because your plan no longer includes it.Move back to a plan that covers QuestDB.
429Too many requests from this address.Honor the Retry-After header. Batching clients never reach this ceiling.
503The database is stopped or archived, or the account is moving.Retryable. Retry-After is set when the wait is known.
502 or 504The database did not answer in time.Retryable. Check the database is running in the dashboard.

Plan and storage

QuestDB is a Pro engine, $15/mo flat, and it runs always-on rather than sleeping on idle, so an ingest pipeline never arrives at a hibernated database. There is no ingestion meter: the plan price does not move with how many rows you write.

What ingested data does consume is storage. Pro includes 25 GB, and each pool block adds another 25 GB for $10/mo. A time-series writer fills storage faster than most workloads, so it is worth watching the database's size in the dashboard and adding a block before you are up against the allowance rather than after. An account at or over its storage allowance gets a warning email and a 24-hour grace window; if it is still over when the window closes, its databases are stopped until usage is back under the allowance. Nothing is deleted, and backups stay downloadable. See the always-on pool for how the pool and its blocks work. Ingest traffic counts toward your plan's transfer allowance the same as any other connection.

Querying what you wrote

Read the data back with SQL over the Postgres wire endpoint. The connection string from the dashboard works as it is: the database hostname on port 5432 with sslmode=require, so psql, every Postgres driver, and GUI clients such as TablePlus and DBeaver connect normally. Do not add sslnegotiation=direct to that string; it is only for connecting on the database's own dedicated port, and on port 5432 it makes the handshake fail.

Read the rows back with psql
psql "postgresql://YOUR_USER:YOUR_PASSWORD@YOUR-DB.cloud.layerbase.dev:5432/qdb?sslmode=require"

-- then
SELECT * FROM sensors LATEST ON timestamp PARTITION BY device;