API Reference

questdb

questdb.connect(conf_str: str | None = None, *, host: str | None = None, port: int | str | None = None, tls: bool | None = None, connection_listener: Callable[[ConnectionEvent], None] | None = None, connection_event_inbox_capacity: int = 0, error_handler: Callable[[SenderError], None] | None = None, error_event_inbox_capacity: int = 0, **params) → QuestDB[source]

Connect to a QuestDB deployment and return a QuestDB handle.

This is the deployment-level entry point and the default for new code. The handle owns connection pools for both ingestion and queries: borrow row-building senders with QuestDB.sender(), bulk-load DataFrames with QuestDB.dataframe(), run SQL with QuestDB.query(), and borrow reader leases with QuestDB.reader(). The standalone Sender is the complementary connection-level API (one sender = one connection) for point-to-point needs: ILP/HTTP transactions, ILP/TCP, QWP/UDP datagrams, and manually driven single ws connections.

Pass either a QWP/WebSocket configuration string (ws::addr=host:port; or wss::...) or the equivalent keywords: host, port (default 9000) and tls (default False) select the endpoint. Either way, any further keyword arguments are added as configuration-string settings (username='u', sender_pool_max=4, …; booleans normally map to on/off, while tls_verify=False maps to unsafe_off); a setting present both in the string and as a keyword raises ValueError, matching Sender.from_conf(). One configuration addresses the whole deployment; list every cluster node in a single addr server list.

By default connect() opens the warm minimums up front and fails fast when the server is unreachable or rejects the credentials. Set lazy_connect=True (or lazy_connect=true in the string) to tolerate a down server at startup: nothing connects here, sender leases buffer locally and connect in the background, and readers connect on the first query.

Pooled row senders auto-flush by default at 1,000 rows, 100 milliseconds, or a cap-derived byte threshold. Set auto_flush_bytes='off' to disable only the byte trigger, or auto_flush='off' to disable all automatic publishing. Auto-triggered publishes do not wait for server acknowledgement; use an explicit flush(wait=True) or wait() as an acknowledgement barrier.

connection_listener receives one ConnectionEvent per connection-state transition; error_handler receives one SenderError per server rejection (without it rejections are logged through the questdb logger). Each runs on its own dispatcher thread; see QuestDB.from_conf() for details. When a handler or listener closes over the returned handle, call QuestDB.close() explicitly (or use with) rather than leaving the handle to the garbage collector.

import questdb

with questdb.connect('ws::addr=localhost:9000;') as db:
    with db.sender() as sender:
        sender.row('trades', symbols={'sym': 'BTC-USD'},
                   columns={'price': 62000.0}, at=ts)
    db.dataframe(df, table_name='trades', at='ts')
    result = db.query('SELECT * FROM trades LIMIT 10')

with questdb.connect(host='localhost', port=9000) as db:
    ...
class questdb.QuestDB

Bases: object

Handle to a QuestDB deployment over QWP/WebSocket.

Owns the connection pool; lends row-building senders via sender(), bulk-loads DataFrames via dataframe(), runs queries via query(), and lends reader leases via reader(). Construct with questdb.connect(). Instances are safe to share across threads.

__enter__()
__exit__(exc_type, exc_val, exc_tb)

Close the handle at the end of a with block.

When the most recent close() on the handle was given a number of seconds, this close waits only for what is left of that budget; otherwise it closes with the limits of a close() with no arguments.

Leaving the block on an exception still closes, but a close that cannot finish is reported through the questdb logger instead of raised: the frames unwinding here are often the ones holding the leases the close is waiting for, so raising would replace the exception being reported with a complaint that follows from it. Leaving the block normally raises as close() does – a lease left open there is a leak worth hearing about.

close(timeout=<default>)

Close the client and its connection pool.

Closing is one-way. Once a close() proceeds – rather than being refused outright, as from inside one of the handle’s own calls or callbacks – the handle refuses new work: sender(), reader(), dataframe(), query() and the rest raise. Work already in flight drains: a call in progress runs to completion, and an outstanding lease keeps working until its holder closes it. The one exception is PooledSender.dataframe(), which forwards to dataframe() over a connection of its own from the pool, so a load started through a lease after close() is new work and is refused like any other. The native pool is torn down by the first close() that finds nothing using the handle, or when the handle itself is collected.

Calls on the handle and on its leases are counted per OS thread. With greenlets – gevent, eventlet, or greenlet used directly – all greenlets on one thread count as one caller, so close() is also refused while another greenlet on the same thread is suspended inside a call on the handle or on one of its leases. See Threading Considerations.

timeout sets how long this call waits for that drain. Calls are the handle’s own methods in progress, such as dataframe() and query(); anything done through a lease counts as that lease, including a long lease.dataframe() already running when close() starts.

  • Left out: a call in progress is waited for until it returns, and an outstanding lease for up to a minute. A long load through a lease needs a larger timeout, or None.

  • A number of seconds: calls and leases alike are waited for up to that long. 0 closes only if nothing is in flight.

  • None: calls and leases alike are waited for until they are done.

close() cannot interrupt a call in progress. A dataframe() reading a stream that never ends, or one waiting on the very thread that calls close(), keeps it waiting until timeout runs out – and, with no limit on calls, for ever. A lease comes back only when its holder closes it, so one held by the calling thread, or by a thread that has since finished, never will: close every lease before closing the handle.

When the limit runs out, this call raises QuestDBError with code set to QuestDBErrorCode.InvalidApiCall, naming how many leases and calls are still outstanding. The handle stays closing – it never goes back to open – and a later close(), with any timeout, resumes the wait and finishes the teardown once the last of them is done. Waiting for a concurrent close() on another thread to finish its teardown is limited too: a number of seconds covers that wait and the drain together; left out, that wait gets a minute of its own; None puts no limit on it.

timeout covers only this waiting. The teardown then spends up to close_flush_timeout_millis (5 seconds by default) draining each sender’s queue. An in-memory queue drops whatever it has not delivered by then; a disk-backed queue (sf_dir) keeps it for replay on restart. The drop is not reported to Python: it raises nothing and logs nothing through the questdb logger. To be sure rows have arrived, close each lease with close(wait=True) before closing the handle.

While it waits, it reports what it is still waiting for through the questdb logger at WARNING: every five seconds for the first minute, then once a minute.

A number of seconds is a budget for the whole close, including the close a with block makes on its way out: when the most recent close() on the handle was given one, leaving the block waits only for what is left of it, and not at all once it has run out. To bound leaving a with block, call close(timeout=...) as the block’s last statement. Otherwise, leaving the block closes with the limits of a close() with no arguments.

Idempotent: closing a closed handle returns without doing anything, and no close() returns success unless the teardown has run – when several race, one of them runs it and the others return once it is done.

When called from inside one of this handle’s own error_handler / connection_listener callbacks, it never waits – while the callback runs, that thread delivers nothing, including whatever a wait would need. It closes immediately when nothing is using the handle; it refuses when leases or calls are outstanding (close from another thread to wait for the drain); and when a concurrent close() is already running it returns without waiting, possibly before that close finishes – the handle only moves toward closed, never back to open.

connection_events_delivered

Total connection events delivered to the listener. 0 when no listener is registered.

connection_events_dropped

Total connection events discarded by the listener inbox’s drop-oldest policy. 0 when no listener is registered.

dataframe(df, *, table_name: str | None = None, table_name_col: None | int | str = None, symbols: str | bool | List[int] | List[str] = 'auto', at: ServerTimestampType | int | str | TimestampNanos | datetime.datetime, max_rows_per_batch: int = 16384, schema_overrides: Dict[str, object] | None = None)

Ingest a dataframe through the pooled columnar QWP path.

Ingestion always uses the direct (non-store-and-forward) column sender, independent of sf_dir. On success, the call returns only after every DataFrame batch has been committed. The first successful batch on a fresh direct connection is already a commit boundary; subsequent batches are pipelined until the final commit. Large Arrow inputs add a checkpoint about every 100 batches to bound the uncommitted tail. The client may checkpoint earlier if deferred capacity fills or a batch must be split to fit.

On a transient connection failure, the client re-sends the original, replayable DataFrame only when no batch was successfully published and the failed operation is not in_doubt. Otherwise it raises rather than replaying from row zero. If a batch from this call may have committed, the raised error has in_doubt=True even when the final native write alone was provably not delivered. This includes local validation and Arrow stream errors after publication. Internal checkpoints do not reset the call’s delivery status. The load did not finish, but any already committed prefix remains; an application-level retry of the whole DataFrame can duplicate it unless the destination table uses suitable DEDUP UPSERT KEYS. A consumed one-shot stream may instead be non-replayable with in_doubt=False when no rows could have landed; that error asks for a fresh reader. Server-side rejections (e.g. a schema mismatch) surface as a plain QuestDBError; the structured sender_error diagnostic is attached only by the store-and-forward senders.

df accepts any of:

  • pandas pandas.DataFrame. NumPy-backed columns route through the legacy planner; pyarrow-backed columns route through the Arrow C Stream capsule path below.

  • polars polars.DataFrame and polars.LazyFrame (polars 1.32 or later). LazyFrame is materialised via .collect(engine='streaming').

  • pyarrow pa.Table, pa.RecordBatch, and pa.RecordBatchReader.

  • Any object exposing the Arrow C Data Interface — i.e. with __arrow_c_stream__ (duckdb / cudf / pyarrow-backed pandas 2.2+) or __arrow_c_array__ (single Arrow array exporters, wrapped into a one-batch pa.Table).

at names the designated timestamp column (by name or index). Alternatively pass a fixed TimestampNanos / datetime shared by every row (encoded as a repeated constant; resubmission stays idempotent under DEDUP UPSERT KEYS), or the explicit ServerTimestamp sentinel to let the server assign each row’s timestamp on arrival — an opt-in mirroring the row API, since server-assigned timestamps defeat DEDUP UPSERT KEYS on resubmission.

The columnar path loads one table per call: name it via table_name — or, for NumPy-backed pandas input, the dataframe’s index name (df.index.name); Arrow-native input (polars, pyarrow, pyarrow-backed pandas) requires an explicit table_name. table_name_col raises UnsupportedDataFrameShapeError — split multi-table frames (e.g. df.groupby(col)) and load each group.

Supports a column-QWP v1 subset: a single per-call table name, non-null designated timestamp column, and the following per-column dtypes:

  • Numeric: NumPy bool/int{8,16,32,64}/uint{8..64}/float{32,64}. Arrow pa.int{8,16,32,64}, pa.float{16,32,64}, and pa.uint{8,16,32,64} are accepted by the Rust Arrow batch route when the frame uses a fixed table name and a designated timestamp column name. Unsigned Arrow values follow the Rust Arrow policy: UInt8 and UInt16 widen to INT, UInt32 to LONG, and UInt64 values up to i64::MAX are accepted as LONG. Larger UInt64 values are rejected because QuestDB QWP-WS encodes integers as signed i64. Signed int8/int16 land as QuestDB INT; the row-oriented Sender.dataframe() instead widens every integer to LONG. Likewise float32 lands here as FLOAT but is widened to DOUBLE by the row path — ingest a given table through a single path to avoid a first-write column-type mismatch.

  • String / Symbol: object-dtype str, pa.string(), pa.large_string(), pd.CategoricalDtype of strings.

  • Timestamp: NumPy datetime64 units accepted by pandas and pa.timestamp with unit s, us, or ns (tz-aware accepted on Arrow-backed columns in the Rust Arrow route). An Arrow ms timestamp field lands as DATE instead — see DATE below — while a NumPy datetime64[ms] field is widened to a microsecond TIMESTAMP unless a df.attrs['questdb'] claim names it DATE. Timestamp field columns accept null timestamps (NaT / None) and values before the Unix epoch; a datetime64[ns] field carrying NaT is re-exported through Arrow so its INT64_MIN null sentinel is preserved (this needs pyarrow). The designated timestamp column (at=) must be non-null and at or after the epoch.

  • Decimal: Arrow-backed pa.decimal{32,64,128,256} columns (pa.decimal32/pa.decimal64 require pyarrow >= 19), or object-dtype columns of decimal.Decimal (re-exported through Arrow; the decimal width and scale are inferred from the values).

  • Array: Arrow pa.list_ / pa.large_list / pa.fixed_size_list columns with a float64 leaf (nested for multi-dimensional), and object-dtype columns of float64 numpy.ndarray cells (any rank; requires pyarrow). Both land as QuestDB ARRAY(DOUBLE). Null rows are allowed; null elements inside an array are not.

  • UUID: object-dtype columns of uuid.UUID, the arrow.uuid extension type over pa.fixed_size_binary(16), or any 16-byte binary column claimed with schema_overrides={'col': 'uuid'}. Bytes are canonical RFC 4122 big-endian — exactly uuid.UUID.bytes — and the client byte-swaps them into QWP wire order. Round-trip through query is byte-identity.

    The arrow.uuid route requires pyarrow >= 18, which is where that canonical extension type is registered, and the column must carry the extension type — build it from pa.uuid(). Writing ARROW:extension:name as plain field metadata is not equivalent: pyarrow leaves such a key on the field, and a pandas ArrowDtype carries a type and no field, so a frame built that way holds a bare pa.fixed_size_binary(16) and is refused as unlabelled — see Binary below. Frames imported over the C data interface or Arrow IPC do get the extension type rebuilt from that key, which is why the same metadata can route two ways. The other two routes — uuid.UUID cells and schema_overrides — are free of both conditions, and a {'kind': 'uuid'} entry in df.attrs['questdb'] claims an object-dtype column of 16-byte bytes, the shape to_pandas(dtype_backend='numpy_nullable') returns. On pyarrow < 18, where no column can carry the label at all, a pandas frame’s bare pa.fixed_size_binary(16) column is still refused rather than quietly landing as BINARY.

  • LONG256: 32-byte binary columns. Bytes are little-endian limbs, least-significant limb first, forwarded verbatim. Three routes claim the type, and they need different inputs. schema_overrides={'col': 'long256'} needs a fully Arrow-backed frame — every column an ArrowDtype, e.g. df.convert_dtypes(dtype_backend='pyarrow'). questdb.column_type=long256 field metadata needs a pa.Table or pa.RecordBatch handed straight to this method: field metadata lives on the field, a pandas ArrowDtype carries a type and no field, so a column that passed through pandas has already lost the claim. A {'kind': 'long256'} entry in df.attrs['questdb'] claims an object-dtype column of Python ints or of 32-byte bytes — the two shapes QueryResult.to_pandas() hands a LONG256 column back in, plain and dtype_backend='numpy_nullable' respectively. Ints are encoded as 32 unsigned little-endian bytes, so every non-null value must satisfy 0 <= value < 2**256; bytes cells go out verbatim and must be exactly 32 bytes. LONG256 has no Arrow extension type, so a pa.fixed_size_binary(32) column in a pandas frame claims nothing by itself and is refused rather than sent as opaque bytes — see Binary below for why pandas frames refuse where native Arrow input writes BINARY.

  • Binary: object-dtype columns of bytes, bytearray, or C-contiguous one-byte-item memoryview cells land as BINARY, the same value types Buffer.row accepts. Arrow pa.binary(), pa.large_binary(), and pa.fixed_size_binary(n) columns also land as BINARY, with the exception and the condition below.

    The exception is pa.fixed_size_binary(8), which both planners refuse with ArrowUnsupportedColumnKind, labelled or not. polars exports its Object dtype in exactly that shape, as handles to objects in its own process rather than data, and once exported nothing else marks it. To store genuine 8-byte values as BINARY, make the column variable-length binary: pa.binary(), pl.Binary, or an object column of bytes.

    The condition is on widths 16 and 32. A width on its own claims nothing: such a column is a UUID or a LONG256 only when something labels it — pa.uuid(), questdb.column_type field metadata of uuid or long256, schema_overrides, or a df.attrs['questdb'] claim. Any other extension type, or any other questdb.column_type value, leaves the column unlabelled — except binary, which, like schema_overrides={'col': 'binary'}, labels it as plain bytes. What happens to an unlabelled one depends on where it comes from, not on which planner writes it. Native Arrow input — a pa.Table, pa.RecordBatch, pa.RecordBatchReader, or any other __arrow_c_stream__ producer — keeps its field metadata, so an unlabelled column there is written as BINARY, and once the write is done the questdb logger records one WARNING per such column naming the remedies. A pandas frame refuses it with BadDataFrame, fully Arrow-backed or not: an ArrowDtype carries a type and no field, so the column may be a UUID or LONG256 column whose label was dropped on the way into pandas, and guessing would auto-create a table with the wrong column type. The refusal does not depend on the pyarrow version — which remedies the message can name does, since pa.uuid() needs pyarrow 18, but which columns are accepted does not. To write either width as BINARY without the warning or the refusal, label it as plain bytes — schema_overrides={'col': 'binary'}, or {"questdb.column_type": "binary"} field metadata on native Arrow input — which sends it as it is, with no copy. Making the column variable-length binary (pa.binary(), pd.ArrowDtype(pa.binary())) or, in pandas, an object-dtype column of bytes works too, at the cost of a copy. Requires QuestDB 10 or newer.

  • DATE: Arrow pa.timestamp('ms') (tz-aware or naive), pa.date32(), and pa.date64() columns land as DATE. The Arrow type is itself the claim, so there is no DATE cell type and no 'date' kind for schema_overrides. Such a column lands as DATE whether the frame is fully Arrow-backed or mixed with NumPy columns. What has no route of its own to DATE is the NumPy datetime64[ms] dtype, which widens to a microsecond TIMESTAMP, and its tz-aware datetime64[ms, tz] form, which is rejected outright. A millisecond column named by at= becomes the table’s designated TIMESTAMP, like any other at column. Round-tripping keeps the type: query returns a DATE column as pa.timestamp('ms', 'UTC'), which to_arrow() and to_pandas(dtype_backend='pyarrow') feed straight back as DATE. Plain to_pandas() gives a NumPy datetime64[ms] column, and the df.attrs['questdb'] claim it comes with names the column DATE, so this method puts the Arrow type back on before writing and that frame lands as DATE too. Row-at-a-time, DATE is written with DateMillis through Buffer.row.

Server-side coercion handles cross-type writes (e.g. pa.string() UUIDs landing in a UUID column are parsed server-side; narrow ints landing in a wider column are widened). Failures surface as QuestDBError from the flush().

schema_overrides reclassifies columns by name, mapping each to 'symbol', 'ipv4', 'char', 'uuid', 'long256', 'binary', or ('geohash', bits) (e.g. {'venue': 'symbol', 'src_ip': 'ipv4'}). Unknown column names are rejected. An override wins over any Arrow field metadata on its column, except 'binary', which is refused on a column whose own type or field metadata labels it UUID or LONG256. 'uuid' and 'long256' apply to fixed-size and variable-length binary columns alike — the route polars frames take, since polars has no fixed-size binary dtype — and every non-null value must be exactly 16 or 32 bytes respectively. 'binary' applies to binary columns and says a 16- or 32-byte column holds plain bytes (see Binary above); it cannot override a pa.uuid() type or a uuid / long256 field label, and is refused on such a column. It requires the Arrow columnar path (fully Arrow-backed input without table_name_col; pandas input needs pandas 2.2 or later); on input that falls back to the NumPy planner it raises UnsupportedDataFrameShapeError.

A pandas frame that came out of QueryResult.to_pandas() carries its source column types in df.attrs['questdb'], and this method reads them back. That is what keeps UUID, LONG256, IPV4, CHAR, GEOHASH, and DATE columns their own type on a read-modify-write round trip: their claim lives in Arrow field metadata, which a pandas dtype — an Arrow type and no field — cannot hold. The metadata recalls what the frame held when it was read, so a column since dropped, renamed, or retyped simply loses its claim, and symbols / schema_overrides outrank it. BYTE, SHORT, and INT columns still widen one step on the way back in; the Arrow ingest API has no override that pins them.

The claim is honoured on every shape a to_pandas backend produces, so plain to_pandas(), dtype_backend='pyarrow' and dtype_backend='numpy_nullable' round-trip the same way. Besides the Arrow-backed and plain NumPy columns, that covers the object columns the other backends leave behind: Python ints, which is what a pandas masked column becomes and what plain to_pandas() returns a LONG256 as, and bytes, which is how dtype_backend='numpy_nullable' returns UUID and LONG256. Frames saved from version 5.0, whose plain to_pandas() returned a GEOHASH column as an unsigned integer, round-trip too: such a column is written as the signed integer of the same width, bit for bit. A frame whose columns were retyped past that set — which a custom types_mapper can do — keeps the claim in attrs and cannot use it: a claimed column that arrives as a float or a string dtype lands as that dtype implies, and the claim it could not use is reported as a warning-level record on the questdb logger. An object column states no width or range of its own, so a claimed value the type cannot hold — an integer past 2**32-1 under ipv4, a cell that is not exactly 16 or 32 bytes under uuid or long256 — is refused rather than written into a column of the claimed type. GEOHASH is the deliberate exception, described next.

Bulk GEOHASH values are raw bit patterns, whatever the column is stored as: NumPy, Arrow, or an object column of Python ints, which is also what a pandas masked column becomes. This is a deliberate contract, the same for every shape, so that whether a frame is written does not depend on whether a GEOHASH column holds a null; plain QueryResult.to_pandas() returns a column that does as a masked column. The declared precision must be in 1..=60 and fit the signed integer carrier, but no per-value check verifies that the pattern has no bits set above that precision. For example, 32 under GEOHASH(5b) is accepted even though it needs six bits. If the declared precision is wrong for the data, the write can succeed and store a different GEOHASH location or NULL. Only the low ceil(precision / 8) bytes are encoded, and the exact result of an inconsistent value is unspecified. This is a semantic data-integrity risk, not a memory-safety risk for otherwise valid NumPy/Arrow buffers. The caller must validate bulk values. The scalar Geohash constructor remains strict; malformed custom Arrow C Data structures remain invalid input and are outside this guarantee.

A column of nothing but nulls names no type of its own, and without a claim it is left out of the write and out of the table the write auto-creates. A claim names one, so a claimed all-null column is written as the claimed type — which is what keeps a batch that happens to hold no value for one column from deciding the shape of the table.

The claim a query result carries reads as an ordinary mapping but cannot be edited in place, because every copy of the frame shares it; assign a new mapping to df.attrs['questdb'] to change it. A plain hand-written dict is read here just the same, in this shape:

df.attrs['questdb'] = {
    'version': 1,
    'columns': {
        'src_ip': {'kind': 'ipv4'},
        'pos': {'kind': 'geohash', 'precision_bits': 20},
        'traded_on': {'kind': 'date'},
    },
}

version must be 1 when present, and a mapping without it reads as version 1. The claim’s vocabulary can gain kinds, and a client applying a version it does not understand would write the wrong column type, which is the outcome the claim exists to prevent, so a mapping carrying any other version is not read and every column in it is ignored. A claim, or a column’s entry in it, that is not read is reported through the questdb logger; the write goes ahead either way. kind is one of 'uuid', 'long256', 'ipv4', 'char', 'geohash', or 'date' — the types whose claim cannot always ride on a pandas dtype. precision_bits accompanies 'geohash' alone. A 'date' claim restores the DATE type after plain pandas has represented it as NumPy datetime64[ms]. Naming a column that is not in the frame is not an error; the entry is skipped.

A pa.Table or pa.RecordBatch from QueryResult.to_arrow() or QueryResult.iter_arrow() carries the same claims in Arrow field metadata, as a questdb.column_type key on every field, and this method passes that metadata to the native client unchanged. A field claim the field’s Arrow type cannot carry — 'short' on pa.int32(), 'ipv4' on pa.int64() — makes this method raise QuestDBError, with code set to QuestDBErrorCode.ArrowIngest, before anything is sent. This stricter rule is deliberate. Field metadata is the input contract of the native Arrow ingest, which refuses such a field in every language that uses it. A field claim also goes stale only when the old field is carried onto a new type: Table.cast with a schema built from field.with_type(...), or pa.concat_tables(..., promote_options='permissive') with the claimed table first. Casting to a schema built from scratch, set_column by name, and a polars or pandas round trip all build fields without the key. df.attrs belongs to the whole frame and survives astype and arithmetic, so a stale claim there is the normal case and is logged instead. To write a retyped column, drop its metadata:

i = table.schema.get_field_index('col')
table = table.cast(table.schema.set(
    i, table.schema.field(i).remove_metadata()))

max_rows_per_batch sets the pipelining granularity, not a safety limit: any batch exceeding the negotiated per-batch byte cap is split regardless of it, and a single row is never bounded by it. Each batch is one unit of client memory and server-side apply. The first successful batch on a fresh connection is a commit boundary. Sliceable Arrow inputs add another checkpoint about every 100 batches, so max_rows_per_batch * 100 rows approximates the maximum periodic uncommitted tail, not a whole-source replay window. The NumPy planner pipelines the remaining batches until its final commit. Either path may checkpoint earlier when deferred capacity fills. Raise max_rows_per_batch for narrow numeric rows; lower it for very wide rows or tight memory. Streaming Arrow input (pa.RecordBatchReader) is not re-batched — the producer’s batch size governs its checkpoint window.

error_events_delivered

Total server rejections delivered to the error_handler (or to the default logging handler when none was registered).

error_events_dropped

Total server rejections discarded by the handler inbox’s drop-oldest policy.

execute(sql, binds=None, *, timeout=None)

Run a statement and discard whatever it returns.

Executes sql like query(), drains the result to its clean end and returns the pooled connection — the pattern DDL and DML otherwise need spelled out as with db.query(sql) as r: r.to_pandas(). Statement output (a COPY status row, admin-function rows, a stray SELECT) is discarded; use query() when you want the result. The connection’s SYMBOL dictionary is left untouched.

binds and timeout behave exactly as on query().

Returns the statement’s EXEC_DONE report as an ExecDone named tuple (op_type, rows_affected), or None for a statement that streamed a result set instead (a SELECT). See QueryResult.exec_done for the caveats on rows_affected.

static from_conf(conf_str, *, connection_listener=None, connection_event_inbox_capacity=0, error_handler=None, error_event_inbox_capacity=0)

Construct a handle from a QWP/WebSocket configuration string.

Prefer the questdb.connect() module-level factory.

By default the handle connects eagerly: construction pre-opens the warm minimums (sender_pool_min sender and query_pool_min reader connections, one of each by default), so an unreachable server or bad credentials fail here, fast. With lazy_connect=true construction opens no connection: sender leases buffer locally and connect in the background, readers connect on first use (query_pool_min defaults to 0), and errors surface from the first operation instead. Combining lazy_connect=true with a blocking initial_connect_retry or a positive query_pool_min is a configuration conflict.

Pooled row auto-flush is enabled by default at 1,000 rows, 100 milliseconds, or an estimated encoded size at 90% of the effective frame cap. Before a server cap is known, or when the server omits it, the byte threshold is 90% of the lower of QuestDB’s default cap, 2 MiB less 14 bytes, and the local store-and-forward frame cap. Set auto_flush_bytes=off to disable only the byte trigger, or auto_flush=off to disable all automatic publishing. The mode is immutable and shared by every lease; the interval starts when the first row enters an empty lease buffer. Auto-triggered publishes do not wait for server acknowledgement.

The underlying connection pool is opened by questdb_db_connect_with_handlers. Dataframe ingestion always uses the direct (non-store-and-forward) QWP/WebSocket column sender, independent of sf_dir. On a transient connection failure the original DataFrame is re-sent only when no batch was successfully published and the failed operation is provably not delivered. Once a prefix may have committed, the call raises instead of replaying from row zero. A delivery-unknown failure surfaces as QuestDBError with in_doubt set. request_timeout bounds each commit’s no-progress ack wait; request_timeout=0 disables that deadline, so a stalled but connected server can block dataframe() indefinitely.

connection_listener, when set, is a callable receiving one ConnectionEvent per connection-state transition of the pool’s ingress connections (initial connect, per-endpoint attempt failures, failover, terminal auth rejection). It runs on a dedicated dispatcher thread fed by a bounded inbox (connection_event_inbox_capacity; 0 selects the default of 64) with a drop-oldest overflow policy, so a slow listener cannot stall ingest or reconnects. Exceptions it raises are logged and swallowed. Dropped/delivered totals are available via connection_events_dropped / connection_events_delivered. Successful events are queued only after negotiated state, including the server frame cap, is committed; they do not acknowledge data.

error_handler, when set, is a callable receiving one SenderError per server rejection recorded by any of the pool’s store-and-forward connections — including rejections for rows published through a PooledSender that was already closed. It runs on its own dedicated dispatcher thread fed by a bounded inbox (error_event_inbox_capacity; 0 selects the default of 64, overflow drops the oldest event). Exceptions it raises are logged and swallowed. Without a handler every rejection is logged through the questdb logger instead — ERROR for terminal rejections, WARNING for retriable ones (the affected rows are replayed, not lost) — so rejections are never silent. A terminal rejection is queued only after the connection’s terminal latch and pollable diagnostic have been committed. Delivered/dropped totals are available via error_events_delivered / error_events_dropped. Use the handler for dead-lettering, alerting, and metrics; terminal failures also surface as QuestDBError from the sender calls themselves.

When a handler or listener closes over the returned handle, call close() explicitly (or use with): until closed, such a handle is pinned alive rather than collected, so an abandoned one leaks instead of crashing. The pin is dropped at interpreter shutdown, so an unclosed self-referential handle can still crash during finalization.

query(sql, binds=None, *, reset_symbol_dict=True, timeout=None)

Execute a SQL query and return a QueryResult.

Egress goes through the QuestDB Wire Protocol (QWP/WebSocket) /read/v1 endpoint. The reader is borrowed from the same connection pool that hosts the ingress writers and is returned to the pool when the returned QueryResult is consumed or closed (a poisoned connection is dropped instead). Auth / TLS settings apply to both directions.

To run several queries on one pooled connection, lease a PooledReader with reader() instead.

Parameters:
  • sql – SQL text to execute. Forwarded verbatim to QuestDB.

  • binds –

    Positional bind parameters matching the $1..``$N`` placeholders in sql, as a list or tuple. Always prefer binds over interpolating values into the SQL text — they take no escaping and keep types exact. Supported Python types and their QuestDB bind types:

    • None → SQL NULL (bound as a VARCHAR null)

    • bool → BOOLEAN

    • int → LONG (must fit signed 64-bit)

    • float → DOUBLE

    • str → VARCHAR

    • datetime.datetime → TIMESTAMP (microseconds; a naive value is interpreted as UTC, the same rule as everywhere else in the API)

    • TimestampMicros → TIMESTAMP

    • TimestampNanos → TIMESTAMP_NS

    • datetime.date → DATE

    • decimal.Decimal → the narrowest DECIMAL that holds it, with its fractional digits as the scale (up to 76 digits and scale 76; NaN and infinities raise ValueError)

    • uuid.UUID → UUID

    Any other type raises TypeError naming the placeholder.

    res = db.query(
        'SELECT * FROM trades WHERE ts > $1 AND sym = $2',
        [datetime.datetime(2026, 7, 1), 'BTC-USD'])
    

  • reset_symbol_dict – When True (the default), the server resets the connection’s SYMBOL dictionary before this query so it never inherits symbols from earlier queries on the pooled connection (query-scoped dict), avoiding cross-query dictionary bloat in to_polars() / to_pandas(). Set False to keep the dictionary warm across repeated identical queries. No-op against servers that predate the capability.

  • timeout – Run this query under its own budget instead of the server-wide query.timeout, overriding the connect string’s query_timeout_ms. An int is milliseconds; a datetime.timedelta works too, with a positive value below one millisecond rounded up to 1 ms. None (the default) keeps the connect string’s query_timeout_ms; 0 clears it. Requires a server advertising CAP_QUERY_TIMEOUT: against an older one a non-zero timeout is refused before anything is sent, with QuestDBErrorCode.UnsupportedServer, rather than silently running under the server default; the connection is untouched, so pass timeout=0 or raise the server’s query.timeout instead. On expiry the error carries QuestDBErrorCode.QueryTimeout and the connection stays usable — but do not retry a write on it: a statement that outlives its timeout is reported as done instead, and a DDL / INSERT / UPDATE that timed out waiting for the table writer may still be applied.

Returns:

A QueryResult. Materialise it via iter_rows(), to_pandas(), to_arrow(), iter_arrow(), iter_pandas(), or the __arrow_c_stream__ PyCapsule protocol.

Sentinel-value collisions in the result frame round-trip QuestDB’s contract: INT64_MIN in a LONG column, NaN in DOUBLE / FLOAT, and the sentinel values for INT / DATE / TIMESTAMP / TIMESTAMP_NS / CHAR / UUID / LONG256 / IPV4 are all interpreted as NULL by QuestDB and cannot be distinguished from legitimate occurrences of those values. GEOHASH has no such sentinel, so a NULL GEOHASH stays distinct from every value.

reader()

Borrow a context-managed PooledReader lease from the pool.

The read-side twin of sender(): the lease holds one pooled reader connection for its lifetime and runs queries on it sequentially via PooledReader.query().

with db.reader() as r:
    r1 = r.query('SELECT * FROM t1').to_pandas()
    r2 = r.query(
        'SELECT * FROM t2',
        reset_symbol_dict=False).to_pandas()

The lease counts as an active use of the handle until it is closed: QuestDB.close() waits for outstanding leases for as long as its timeout allows (a minute by default – see close), and a lease keeps working while the handle drains.

reap_idle()

Manually reap idle above-pool-size connections.

sender()

Borrow a context-managed row-building sender from the pool.

The lease counts as an active use of the handle until it is closed: QuestDB.close() waits for outstanding leases for as long as its timeout allows (a minute by default – see close), and a lease keeps working while the handle drains – apart from PooledSender.dataframe(), which is new work on the handle and is refused once close() has started.

server_info() → ServerInfo

Return a ServerInfo snapshot of the server’s SERVER_INFO handshake: cluster role, failover epoch, negotiated capabilities, handshake wall-clock, and cluster/node identifiers.

A reader is borrowed from the connection pool (opening one on first use, exactly like query()), sampled, and returned to the pool. The snapshot describes that connection at its last handshake; a later failover is reflected only in snapshots taken after it.

class questdb.PooledSender

Bases: object

A row-building sender borrowed from a QuestDB pool.

Obtain a lease with QuestDB.sender(); close() returns the native sender to the pool. Rows publish into an ordered, store-and-forward-covered QWP stream over one pooled connection.

Rows go through row(), dataframe(), flush(), wait() and close(); len(sender) is the number of buffered rows. Frame-level delivery tracking is available through flush_and_get_fsn(), flush_and_keep_and_get_fsn(), published_fsn(), acked_fsn() and await_acked_fsn(), and per-lease server diagnostics through poll_error() and error_events_dropped(). Prefer QuestDB.dataframe() for bulk loads; the lease’s own dataframe() is a convenience that routes to the same direct columnar path. Row auto-flush is enabled by default at 1,000 rows, 100 milliseconds, or a cap-derived byte threshold, and can be configured through the parent handle’s connection settings.

A lease is used by one thread at a time. It may move to another thread – borrowed on one and used on a worker, say – provided the hand-off is synchronized and no two threads call into it at once. To ingest from several threads, share the QuestDB handle and borrow one lease per thread. Every method holds the lease’s lock for its whole call, dataframe() included, so a call from a second thread waits until the running one returns. Code that runs inside a call – a column value’s conversion, the Arrow stream dataframe() reads, or a handler on the questdb logger, which dataframe() logs its warnings to – therefore must not wait on another thread that uses the same lease.

Close every lease, or use it in a with block. A lease collected without close() returns its sender to the pool without sending the rows still buffered in it, and reports how many it discarded through the questdb logger at WARNING.

__enter__()
__exit__(exc_type, exc_val, exc_tb)
acked_fsn()

Highest FSN completed on the lease’s pooled connection by ACK or drop-and-continue rejection, or None if no frame has completed yet. A completed frame is not necessarily applied: a rejected frame the queue drops to make progress also advances this watermark.

await_acked_fsn(fsn, timeout_millis=0)

Wait until the acknowledgement watermark reaches fsn (as returned by flush_and_get_fsn() on this lease).

Returns True once fsn is acknowledged, or False if the no-progress timeout elapsed before fsn was acknowledged (0 waits indefinitely). Only a terminal connection failure raises.

close(flush: bool = True, wait: bool = False)

Return this sender to its pool. Idempotent.

Pending rows are published by default; delivery is owned by the store-and-forward queue, which keeps delivering after the sender is returned. wait=True additionally waits for an OK ack before returning the sender to the pool; without it, a later server rejection of this lease’s rows is reported through the pool’s error_handler (default: the questdb logger).

dataframe(df, *, table_name: str | None = None, table_name_col: None | int | str = None, symbols: str | bool | List[int] | List[str] = 'auto', at: ServerTimestampType | int | str | TimestampNanos | datetime.datetime, max_rows_per_batch: int = 16384, schema_overrides: Dict[str, object] | None = None)

Bulk-load a whole DataFrame over a direct columnar connection borrowed from the pool for the duration of this call.

Prefer QuestDB.dataframe() — this convenience forwards to the same path and is not part of this sender’s row stream: the frame is committed over its own connection and becomes visible to SQL immediately, without waiting for rows appended to this sender with row() to drain. There is therefore no ordering relationship between dataframe() and buffered rows — dataframe() does not flush them; publish those with flush().

Being the handle’s own path, it is refused once QuestDB.close() has started, as QuestDB.dataframe() is, even though this lease’s other methods keep working until it is closed. A load already running when close() starts finishes.

Arguments mirror QuestDB.dataframe().

error_events_dropped()

Diagnostics dropped from the lease’s connection ring (error_inbox_capacity).

flush(*, wait=False)

Publish and clear buffered rows.

By default this returns after local store-and-forward acceptance. Pass wait=True to wait for the server’s OK acknowledgement of everything published through this lease. The wait is a pure ack barrier: only a terminal connection failure raises. Server rejections are delivered to the pool’s error_handler (default: the questdb logger) instead; retriable ones are replayed by the store-and-forward queue.

flush_and_get_fsn()

Publish and clear buffered rows, returning the frame sequence number (FSN) of the published frame, or None if the buffer was empty.

FSNs are watermarks of the lease’s pooled connection: use them for progress tracking while this lease is held (see await_acked_fsn()); they are not portable receipts across leases, which may borrow different connections. Configure auto_flush=off when this call must publish and identify one whole application batch; auto-flush may already have published and cleared some or all buffered rows.

flush_and_keep_and_get_fsn()

Publish buffered rows without clearing the buffer, returning the published frame’s FSN, or None if the buffer was empty.

poll_error()

Poll the next server-rejection diagnostic recorded on the lease’s connection since this lease was borrowed, as a SenderError, or None when none is pending.

The pool’s error_handler independently receives every rejection the moment it is recorded; polling here is a per-lease pull alternative for code that wants diagnostics inline.

published_fsn()

Highest FSN published locally on the lease’s pooled connection, or None if nothing has been published on it yet.

row(table_name: str, *, symbols: Dict[str, str | None] | None = None, columns: Dict[str, None | bool | int | float | str | TimestampMicros | TimestampNanos | datetime.datetime | numpy.ndarray | Decimal | uuid.UUID | ipaddress.IPv4Address | bytes | bytearray | memoryview | Char | DateMillis | Long256 | Geohash] | None = None, at: ServerTimestampType | TimestampNanos | datetime.datetime)

Append one row to this sender’s QWP buffer.

When pooled auto-flush is enabled on the QuestDB handle, completing a row that breaches a configured row, byte, or interval threshold publishes the buffer without waiting for an acknowledgement. A buffer whose size estimate reaches the connection’s frame cap is published too, even with auto_flush_bytes=off. Every published frame fits the frame cap: when the whole buffer does not, or its size estimate is past the cap, the rows buffered before this one go out as a frame of their own and this row starts the next batch, so one call publishes at most two frames. Re-adding the row converts its values a second time.

Whatever this method raises, the row was not added. QuestDBErrorCode.BatchTooLarge means either it does not fit in a frame on its own, or the rows buffered before it do not fit even without it, which can happen after the frame cap shrinks, for example on reconnect; those rows are then discarded, and the error says how many. Any other error raised by a publish propagates from this method and leaves the rows buffered before it in place.

See Buffer.row for supported column types, protocol restrictions, server requirements, and NULL-sentinel behavior.

wait(timeout_millis=0)

Wait for everything published through this lease to receive an OK ack; returns immediately if this lease published nothing.

The wait is a pure ack barrier: only a terminal connection failure raises. Server rejections are delivered to the pool’s error_handler (default: the questdb logger) instead.

timeout_millis is a no-progress timeout; 0 waits indefinitely.

class questdb.PooledReader

Bases: object

A reader lease borrowed from a QuestDB pool.

The read-side twin of QuestDB.sender(): obtain a lease with QuestDB.reader(); it holds one pooled reader connection for its lifetime and runs queries on it sequentially via query(). close() (or leaving the with block) releases the connection: back to the pool if the last query reached its terminal on a live connection (a clean drain, a successful cancel(), or a server-side error such as a parse error or a query timeout), dropped otherwise.

Queries are strictly sequential — one result at a time. Fully drain (or close()) each QueryResult before calling query() again; running the next query while the previous result is still open raises QuestDBError. Closing an undrained result terminates the lease; call result.cancel() before result.close() to preserve it.

Because every query shares one connection, passing reset_symbol_dict=False to follow-up queries keeps the connection’s SYMBOL dictionary warm across them instead of resetting it per query.

The lease counts as an active use of the QuestDB handle (QuestDB.close() waits for it) and has thread affinity: use one lease per thread, on the thread that created it. A QueryResult returned by the lease may be handed to a worker under that result’s thread hand-off rules; wait for it to finish before using the lease again.

__enter__()
__exit__(exc_type, exc_val, exc_tb)
close()

Release the lease’s reader connection. Idempotent.

A still-open (undrained) last result is freed first, which tears down the connection; a connection whose last query reached its terminal (a clean drain, a successful cancel(), or a server-side error) is returned to the pool, any other is dropped and the pool refills on demand.

execute(sql, binds=None, *, timeout=None)

Run a statement on the lease’s connection and discard whatever it returns.

Mirrors QuestDB.execute(), including the ExecDone (op_type, rows_affected) return value. The result is drained to its clean end, so the lease stays usable for the next call, and the connection’s SYMBOL dictionary is left untouched — an interleaved statement does not invalidate a warm dictionary built with reset_symbol_dict=False.

query(sql, binds=None, *, reset_symbol_dict=True, timeout=None) → QueryResult

Execute a SQL query on the lease’s connection and return a QueryResult.

sql, binds, reset_symbol_dict and timeout behave exactly as on QuestDB.query(), except the query runs on the reader this lease holds instead of a per-call pool borrow. The previous query’s result must be fully drained (or closed) first; reset_symbol_dict=False reuses the connection’s SYMBOL dictionary built up by the lease’s earlier queries.

class questdb.Sender

Bases: object

Ingest data into QuestDB over a single connection.

This is the connection-level API: one sender drives exactly one connection (ILP/HTTP, ILP/TCP, QWP/UDP, or a single QWP/WebSocket connection) and carries the point-to-point capabilities the deployment-level handle does not: HTTP transactions, UDP datagrams, and manual ws progress and buffer control. For pooled ingestion and queries, prefer questdb.connect().

See the Sending Data documentation for more information.

__enter__() → Sender

Call Sender.establish() at the start of a with block.

__exit__(exc_type, exc_val, exc_tb)

Flush pending and disconnect at the end of a with block.

If the with block raises an exception, any pending data will NOT be flushed.

This is implemented by calling Sender.close().

__init__(*args, **kwargs)
acked_fsn()

Highest QWP/WebSocket frame sequence number completed by ACK or drop-and-continue rejection.

auto_flush

Auto-flushing is enabled.

Consult the .auto_flush_rows, .auto_flush_bytes and .auto_flush_interval properties for the current active thresholds.

Over ws and wss the byte threshold is on by default, so auto-flush stays on with auto_flush_rows and auto_flush_interval both off. Pass auto_flush=off to batch by hand.

auto_flush_bytes

Byte-count threshold for the auto-flush logic, or None if disabled or not yet known.

Over ws and wss the threshold is at most 90% of the connection’s frame cap, and by default it is 90% of that cap. The cap is known once Sender.establish() has connected, so before that the default reports None. A server that does not advertise a cap counts as having QuestDB’s default, 2 MiB less 14 bytes, unless the local limit is lower. An explicit threshold reads as configured before Sender.establish() and after Sender.close(); the cap lowers it only between the two.

Raises QuestDBError (InvalidApiCall) when read over ws or wss from this sender’s own error_handler or connection_listener.

auto_flush_interval

Time interval threshold for the auto-flush logic, or None if disabled.

auto_flush_rows

Row count threshold for the auto-flush logic, or None if disabled.

await_acked_fsn(fsn, timeout_millis=0)

Wait until the QWP/WebSocket completion watermark reaches fsn.

Returns True once every frame published so far (which includes fsn) has been acknowledged, or False if the no-progress timeout elapsed before the acknowledgement watermark reached fsn.

close(flush=True)

Disconnect.

This method is idempotent and can be called repeatedly.

Once a sender is closed, it can’t be re-used.

Parameters:

flush (bool) – If True, flush the internal buffer before closing. For QWP/WebSocket, this also drains already-published frames before closing.

Refused while a row is part-way through the sender’s internal buffer, which is the buffer this call flushes. A buffer of your own from Sender.new_buffer() is neither flushed nor checked here: the sender is never told which buffers exist, so it cannot refuse on their behalf. Such a buffer keeps every byte written into it, but a sender closed out from under it can no longer send it — finish the row and flush it before closing.

close_drain()

Stop accepting new QWP/WebSocket publications and wait for already published frames to resolve.

connection_events_delivered

Total connection events delivered to the listener. 0 when no listener is registered.

connection_events_dropped

Total connection events discarded by the listener inbox’s drop-oldest policy. 0 when no listener is registered.

dataframe(df, *, table_name: str | None = None, table_name_col: None | int | str = None, symbols: str | bool | List[int] | List[str] = 'auto', at: ServerTimestampType | int | str | TimestampNanos | datetime.datetime, max_rows_per_batch: int = 16384, schema_overrides: Dict[str, object] | None = None)

Write a Pandas DataFrame to QuestDB.

Over ILP/HTTP and ILP/TCP the frame is serialized into the internal row buffer; over QWP/UDP into fire-and-forget datagrams, with the same delivery caveats as row(). Over QWP/WebSocket the frame is bulk-loaded through a poolless direct columnar connection opened from this sender’s configuration for the call (the same direct path as QuestDB.dataframe(), carrying the sender’s auth/TLS regardless of how it was constructed); max_rows_per_batch applies only on that path, and passing schema_overrides on any other protocol raises. The direct load has no ordering relationship with rows buffered via row() and does not flush them.

table_name_col follows the same split. The row-serializing protocols accept it, because they can change table between rows. QWP/WebSocket writes one table per call and rejects it with UnsupportedDataFrameShapeError; split the frame and make one call per table, or send those rows with row().

Example:

import pandas as pd
import questdb as qi

df = pd.DataFrame({
    'car': pd.Categorical(['Nic 42', 'Eddi', 'Nic 42', 'Eddi']),
    'position': [1, 2, 1, 2],
    'speed': [89.3, 98.2, 3, 4],
    'lat_gforce': [0.1, -0.2, -0.6, 0.4],
    'accelleration': [0.1, -0.2, 0.6, 4.4],
    'tyre_pressure': [2.6, 2.5, 2.6, 2.5],
    'ts': [
        pd.Timestamp('2022-08-09 13:56:00'),
        pd.Timestamp('2022-08-09 13:56:01'),
        pd.Timestamp('2022-08-09 13:56:02'),
        pd.Timestamp('2022-08-09 13:56:03')]})

with qi.Sender.from_env() as sender:
    sender.dataframe(df, table_name='race_metrics', at='ts')

Over QWP/WebSocket, supported column types and arguments mirror QuestDB.dataframe, including UUID, IPV4, BINARY, GEOHASH, LONG256, CHAR, DATE, schema_overrides and round-trip claims in df.attrs['questdb']. Over ILP and QWP/UDP, see Buffer.dataframe for the row-serializing mappings; max_rows_per_batch and schema_overrides do not apply on those protocols.

Additionally, this method also supports auto-flushing the buffer as specified in the Sender’s auto_flush constructor argument. Auto-flushing is implemented incrementally, meanting that when calling sender.dataframe(df) with a large df, the sender may have sent some of the rows to the server already whist the rest of the rows are going to be sent at the next auto-flush or next explicit call to Sender.flush().

In case of data errors with auto-flushing enabled, some of the rows may have been transmitted to the server already.

drive_once()

Drive one QWP/WebSocket progress step for manual progress senders.

error_events_dropped()

Number of QWP/WebSocket diagnostics dropped from the bounded ring.

establish()

Prepare the sender for use.

If using ILP/HTTP this will initialize the HTTP connection pool.

If using ILP/TCP this will cause connection to the server and block until the connection is established.

If the TCP connection is set up with authentication and/or TLS, this method will return only after the handshake(s) is/are complete.

flush(buffer=None, clear=True, transactional=False)

If called with no arguments, immediately flushes the internal buffer.

Alternatively you can flush a buffer that was constructed explicitly by passing buffer.

The buffer will be cleared by default, unless clear is set to False.

This method does nothing if the provided or internal buffer is empty.

Parameters:

buffer – The buffer to flush. If None, the internal buffer is flushed.

With QWP/WebSocket, this publishes the buffer into the local sender queue and returns before the server necessarily ACKs the frame. Later terminal diagnostics fail subsequent sender calls and are available as QuestDBError.sender_error. Server diagnostics are also available through Sender.poll_error().

Parameters:
  • clear – If True, the flushed buffer is cleared (default). If False, the flushed buffer is left in the internal buffer. Note that clear=False is only supported if buffer is also specified.

  • transactional – If True ensures that the flushed buffer contains row for a single table, ensuring all data can be written transactionally. This feature requires ILP/HTTP and is not available when connecting over TCP. Default: False.

If the flush fails, the internal buffer is cleared, rows and all; a buffer passed as buffer is left as it is. Over QWP/WebSocket a buffer is published as one frame, and one whose encoded frame exceeds the connection’s frame cap raises QuestDBError (BatchTooLarge) without publishing anything.

The Python GIL is released during the network IO operation.

flush_and_get_fsn(buffer=None)

Publish a QWP/WebSocket buffer locally, clear it on success, and return the assigned frame sequence number.

flush_and_keep_and_get_fsn(buffer=None)

Publish a QWP/WebSocket buffer locally without clearing it and return the assigned frame sequence number.

static from_conf(conf_str, *, bind_interface=None, username=None, password=None, token=None, token_x=None, token_y=None, auth_timeout=None, tls_verify=None, tls_ca=None, tls_roots=None, tls_roots_password=None, max_buf_size=None, retry_timeout=None, retry_max_backoff=None, request_min_throughput=None, request_timeout=None, auto_flush=None, auto_flush_rows=None, auto_flush_bytes=None, auto_flush_interval=None, max_datagram_size=None, multicast_ttl=None, qwp_ws_progress=None, error_handler=None, connection_listener=None, connection_event_inbox_capacity=0, protocol_version=None, init_buf_size=None, max_name_len=None)

Construct a sender from a configuration string.

The additional arguments are used to specify additional parameters which are not present in the configuration string.

Note that any parameters already present in the configuration string cannot be overridden.

static from_env(*, bind_interface=None, username=None, password=None, token=None, token_x=None, token_y=None, auth_timeout=None, tls_verify=None, tls_ca=None, tls_roots=None, tls_roots_password=None, max_buf_size=None, retry_timeout=None, retry_max_backoff=None, request_min_throughput=None, request_timeout=None, auto_flush=None, auto_flush_rows=None, auto_flush_bytes=None, auto_flush_interval=None, max_datagram_size=None, multicast_ttl=None, qwp_ws_progress=None, error_handler=None, connection_listener=None, connection_event_inbox_capacity=0, protocol_version=None, init_buf_size=None, max_name_len=None)

Construct a sender from the QDB_CLIENT_CONF environment variable.

The environment variable must be set to a valid configuration string.

The additional arguments are used to specify additional parameters which are not present in the configuration string.

Note that any parameters already present in the configuration string cannot be overridden.

init_buf_size

The initial capacity of the sender’s internal buffer.

max_name_len

Maximum length of a table or column name.

new_buffer()

Make a new configured buffer.

The buffer is set up with the configured init_buf_size and max_name_len, and matches the sender’s protocol.

Must be called after Sender.establish() and before Sender.close(); otherwise raises QuestDBError (InvalidApiCall).

poll_error()

Poll the next structured QWP/WebSocket diagnostic.

protocol_version

The protocol version used by the sender.

Protocol version 1 is retained for backwards compatibility with older QuestDB versions.

Protocol version 2 introduces binary floating point support and the array datatype.

published_fsn()

Highest QWP/WebSocket frame sequence number published locally.

row(table_name: str, *, symbols: Dict[str, str | None] | None = None, columns: Dict[str, None | bool | int | float | str | TimestampMicros | TimestampNanos | datetime.datetime | numpy.ndarray | Decimal | uuid.UUID | ipaddress.IPv4Address | bytes | bytearray | memoryview | Char | DateMillis | Long256 | Geohash] | None = None, at: TimestampNanos | datetime.datetime | ServerTimestampType)

Write a row to the internal buffer.

This may be sent automatically depending on the auto_flush setting in the constructor.

See Buffer.row for supported column types, protocol restrictions, server requirements, and NULL-sentinel behavior.

Raises QuestDBError (InvalidApiCall) if called while another row() on this sender is still in progress, or a dataframe() over tcp, http or udp: from a column value whose conversion runs Python code, or from a logging handler that writes the questdb logger’s records through this sender. Such a dataframe() writes the frame into this sender’s buffer, and a row written while it runs could not be flushed until it returned. Over ws and wss the frame goes out on a connection of its own, so a row written from inside it is accepted and flushed as usual.

Over ws and wss with auto-flush on, it also raises InvalidApiCall, without adding the row, when called from this sender’s own error_handler or connection_listener. That includes a logging handler that writes the questdb logger’s records through this sender while no error_handler is set, since server rejections are then logged from inside that callback.

Over ws and wss, auto-flush also publishes a buffer whose size estimate reaches the connection’s frame cap, even with auto_flush_bytes=off. Every published frame fits the frame cap: when the whole buffer does not, or its size estimate is past the cap, the rows buffered before this one go out as a frame of their own and this row starts the next batch, so one call publishes at most two frames. Re-adding the row converts its values a second time.

Over ws and wss, whatever this method raises, the row was not added. QuestDBError (BatchTooLarge) means either it does not fit in a frame on its own, or the rows buffered before it do not fit even without it, which can happen after the frame cap shrinks, for example on reconnect; those rows are then discarded, and the error says how many. Any other error raised by a publish also clears the rows buffered before it, as Sender.flush() does.

transaction(table_name: str)

Start a HTTP Transactions block.

class questdb.SenderTransaction

Bases: object

A transaction for a specific table.

Transactions are only supported with ILP/HTTP.

The sender API can only operate on one transaction at a time.

To create a transaction:

with sender.transaction('table_name') as txn:
    txn.row(..)
    txn.dataframe(..)
__enter__()
__exit__(exc_type, exc_val, exc_tb)
commit()

Commit the transaction.

A commit is also automatic at the end of a successful with block.

This will flush the buffer.

dataframe(df, *, symbols: str | bool | List[int] | List[str] = 'auto', at: ServerTimestampType | int | str | TimestampNanos | datetime.datetime)

Write a dataframe for the table in the transaction.

The table name is taken from the transaction.

rollback()

Roll back the transaction.

A rollback is also automatic at the end of a failed with block.

This will clear the buffer. When rollback() is called from code that a row() or dataframe() on this sender runs while it writes, such as a column value’s conversion, it raises QuestDBError (InvalidApiCall) and leaves the transaction as it was.

row(*, symbols: Dict[str, str | None] | None = None, columns: Dict[str, None | bool | int | float | str | TimestampMicros | TimestampNanos | datetime.datetime | numpy.ndarray | Decimal] | None = None, at: ServerTimestampType | TimestampNanos | datetime.datetime)

Write a row for the table in the transaction.

The table name is taken from the transaction.

Note: Support for NumPy arrays (numpy.array) requires QuestDB server version 9.0.0 or higher.

UUID, IPV4, BINARY, CHAR, DATE, LONG256, and GEOHASH columns are QWP-only and are not supported by SenderTransaction, which is ILP/HTTP-only by construction.

class questdb.QueryResult(cursor_handle)

Bases: object

Result of QuestDB.query(sql).

Streams query rows as Arrow record batches. Single-use: each materialisation method (to_pandas, to_arrow, iter_arrow, iter_pandas, or the __arrow_c_stream__ PyCapsule protocol) consumes the underlying cursor; the second consumption raises QuestDBError. For the same reason it cannot be copied.

Consumption rule: fully drain the result, use it as a context manager (with db.query(...) as result:), or call close(). Closing a partial result drops its connection. Call cancel() before closing to preserve it.

Thread hand-off: a QueryResult may be transferred to another thread and consumed, cancelled, closed, or dropped there. Use a normal synchronisation hand-off (for example, Thread.start / join or a thread-safe queue), and access it from only one thread at a time. Concurrent operations on one result are unsupported.

__arrow_c_stream__ is native — the cursor’s record batches are exposed directly through the Arrow C Data Interface, so polars (polars.DataFrame(result), polars 1.32 or later), duckdb (duckdb.from_arrow(result), duckdb 1.2 or later) and other Arrow-native consumers can read query results without pyarrow installed. duckdb reads when you fetch, so fetch before the result is closed. to_pandas / iter_pandas are also pyarrow-free by default (a native numpy path); they only need pyarrow when dtype_backend / types_mapper is passed. to_arrow / iter_arrow / to_polars / iter_polars do require pyarrow.

SYMBOL columns: to_polars / to_pandas build the Categorical directly, interning the connection dictionary once (no per-row remap). to_arrow / iter_arrow / __arrow_c_stream__ emit a generic Arrow form whose per-batch SYMBOL dictionary is compacted to the values each batch uses, which a generic consumer reconciles. So when the target is a polars / pandas frame, the dedicated methods avoid the re-reconciliation that polars.DataFrame(result) / to_arrow().to_pandas() pay on SYMBOL-heavy results. to_arrow().to_pandas() needs pyarrow 23 or later for SYMBOL columns, which use uint32 dictionary indices.

Example:

with db.query('SELECT * FROM trades WHERE ts > $1',
              [datetime.datetime(2026, 7, 1)]) as result:
    df = polars.DataFrame(result)              # no pyarrow
    # df = result.to_pandas()                   # no pyarrow
    # rows = duckdb.from_arrow(result).fetchall()  # no pyarrow
    # table = pa.table(result)                  # pyarrow required
__arrow_c_schema__()

Arrow schema PyCapsule protocol (no pyarrow needed).

It reads the first batch to get the schema, and keeps that batch for __arrow_c_stream__. duckdb 1.5 calls it, so that duckdb.from_arrow(result) opens the stream only once. pa.schema(result) and polars.Schema(result) call it too. After this call, read the result only through __arrow_c_stream__ (pa.table(result), polars.DataFrame(result), duckdb): the other readers raise InvalidApiCall on their first read. When the schema was read first, later calls return the same schema, also after the stream was taken.

__arrow_c_stream__(requested_schema=None)

Arrow C stream PyCapsule protocol (no pyarrow needed). SYMBOL columns arrive compact — each batch’s dictionary holds only the values it references — so a consumer that unifies per-batch dictionaries (e.g. polars.DataFrame(result) on polars 1.32 or later) reconciles them. Older polars misreads such a stream, so when polars before 1.32 reads the result directly (polars.DataFrame(result), polars.from_arrow(result)), the call raises InvalidApiCall and the result stays unread. A stream that reaches polars through another library is not checked.

requested_schema is ignored, also when it does not match: the stream always has the result’s own schema, so check the schema you get. pa.table(result, schema=...) and polars.DataFrame(result, schema=...) cast the result; pa.RecordBatchReader.from_stream(result, schema=...) (pyarrow 15 or later) returns the result’s own schema.

The stream releases the result’s connection when the consumer reads the last batch, not when it releases the stream. A reader lease can run its next query as soon as the stream is drained.

The stream does not replay a result: after a batch has been delivered, a failover ends the stream with an error. The consumer raises its own exception type, and the message contains [FailoverWouldDuplicate]; re-issue the query. polars raises a ComputeError, but polars before 1.37.1 raises a PanicException, which except Exception does not catch.

The returned stream may be handed to a consumer worker thread. The cursor lock serialises the native cursor calls, not the stream’s own state, so the Arrow C stream must be consumed by only one thread at a time; concurrent get_next / release calls are unsupported.

cancel()

Cancel the query and drain to terminal.

The result remains open; call close() before starting another query on the same reader. On failure, the result is closed. Idempotent after success. Rows already decoded but not yet handed out by iter_rows() are not yielded afterwards, and a result not yet read cannot be read afterwards: columns(), iter_rows() and the materialisation methods raise QuestDBErrorCode.InvalidApiCall.

Cancel between rows, not from a signal handler that interrupts a batch being decoded on the same thread: that call is refused with QuestDBErrorCode.InvalidApiCall, because it would invalidate the batch under the decoder.

close()

Release the cursor and reader. Idempotent.

Terminal connections return to the pool; others are dropped. This does not cancel the query. Outstanding iterators become invalid.

columns()

Column names and QuestDB types as [(name, type_name), ...].

type_name is the QuestDB DDL spelling — VARCHAR, TIMESTAMP, TIMESTAMP_NS, SYMBOL, IPv4, GEOHASH(8c), DECIMAL(18,2), DOUBLE[] — matching ColumnType on the server, so it can be compared against tables() / information_schema.questdb_columns() without normalising. Use select ... limit 0 to read a schema without fetching rows; such a probe is read to its end here, so its connection goes back to the pool.

Does not consume the rows — iter_rows() continues from the same stream — but does consume the result, so it rules out to_pandas(), to_arrow() and the other materialisation methods. It decodes no rows, is cheap to call repeatedly, and once read still answers after close().

A result that returned rows is not at its terminal until they have been read, so closing it straight after columns() drops the connection (and on a PooledReader lease makes the lease terminal). Iterate the rows, or call cancel() before close(), to keep it; a LIMIT 0 probe needs neither.

Empty for a statement that ships no result set at all (a non-SELECT); see exec_done for that outcome.

Three limits are the protocol’s, not this client’s:

  • A DECIMAL column is reported with its storage width’s maximum precision (18, 38 or 76) because the wire carries only the scale: a column declared DECIMAL(10,2) reads back as DECIMAL(18,2). Compare on scale when that matters.

  • A legacy STRING column is reported as VARCHAR: QWP has no separate STRING column kind.

  • An array column is reported as DOUBLE[] / LONG[] whatever its dimensionality, because the wire schema carries no dimension count; the server spells a 2-D column DOUBLE[][].

with db.query('select * from trades limit 0') as result:
    for name, type_name in result.columns():
        print(name, type_name)
property exec_done

The statement’s terminal EXEC_DONE report, or None.

An ExecDone (op_type, rows_affected) once a non-SELECT has been drained to its clean end; None before that, and None for a statement that streamed a result set instead (a SELECT ends with END, which carries no counts).

rows_affected is meaningful for INSERT and UPDATE, and 0 for a statement that carries no count — one executed at parse time (TRUNCATE, RENAME TABLE, SET, ALTER TABLE ... RESUME WAL, …). op_type is the server’s QWP operation-type byte, carried through as an opaque int: its values are the server’s, not part of this client’s API. Still available after close().

with db.query('insert into t values (1)') as result:
    list(result.iter_rows())    # drains it
print(result.exec_done.rows_affected)
iter_arrow()

Iterate result batches as pyarrow.RecordBatch.

Streaming: a mid-query failover after the first batch has been yielded surfaces QuestDBErrorCode.FailoverWouldDuplicate (the already-yielded batches cannot be discarded); re-issue the query. If the iterator is abandoned partway, cleanup runs at the next garbage-collection cycle; call close() (or use the context- manager) for deterministic release.

iter_pandas(*, dtype_backend=None, types_mapper=None)

Iterate result batches as pandas.DataFrame.

Mirrors to_pandas(): with no arguments each batch is materialised straight into numpy (no pyarrow, sentinel-preserving). dtype_backend / types_mapper select the pyarrow-backed path instead. Either way every batch carries df.attrs['questdb'], a GEOHASH column gets the same signed integer type, and what a custom types_mapper costs the round trip is the same as it is there.

iter_polars()

Iterate result batches as polars.DataFrame.

Mirrors to_polars() per batch (same Categorical SYMBOL handling) for streaming / low-peak-memory consumption. Every batch’s SYMBOL Categoricals share one persistent Categories identity, so polars.concat over the yielded frames stitches without a categories-mismatch error. If the Categorical fast path fails, it issues one RuntimeWarning and builds the remaining frames with polars.from_arrow, cast to the same Categories. If that cast fails too, the warning says so: those frames have a plain Categorical dtype, and polars.concat with the frames before fails.

Streaming: a mid-query failover after the first batch has been yielded surfaces QuestDBErrorCode.FailoverWouldDuplicate; re-issue the query. Requires polars and pyarrow.

iter_rows()

Iterate the result as tuples of Python objects.

For DB-API-shaped callers — a PEP 249 cursor, dbt, plain scripting — and for small result sets where exact QuestDB types matter more than throughput: metadata lookups, test rows, limit 0 schema probes. Needs neither pyarrow nor pandas. Large analytical results belong on to_pandas() / to_arrow(), which stay columnar; a row of Python objects costs an allocation per cell.

Streams one batch at a time: the batch being decoded is the only one held in memory. None marks a SQL NULL in every column.

Value mapping:

QuestDB

Python

BOOLEAN

bool

BYTE, SHORT, INT, LONG

int

FLOAT, DOUBLE

float (never NaN: the server sends NULL)

VARCHAR, SYMBOL

str

CHAR

one-character str; code point 0 is None

TIMESTAMP

datetime (UTC-aware, microseconds)

TIMESTAMP_NS

datetime (UTC-aware), rounded down to microseconds

DATE

datetime (UTC-aware, milliseconds)

UUID

uuid.UUID

LONG256

int (unsigned)

DECIMAL(p,s)

decimal.Decimal (scale preserved)

GEOHASH(nc)

str (base-32, or ‘0’/’1’ bits when n is not a multiple of 5)

IPv4

ipaddress.IPv4Address

BINARY

bytes

DOUBLE[]

numpy.ndarray

Datetimes are timezone-aware UTC, matching what to_arrow() labels them. TIMESTAMP_NS is rounded down (towards the past) because datetime has no nanosecond field; use to_pandas() or to_arrow() for full precision. A temporal value outside datetime’s years 1..9999 raises QuestDBErrorCode.InvalidTimestamp naming the column. Like any other error raised while decoding, it closes the result before its terminal, so the connection is dropped (on a PooledReader lease, the lease becomes terminal). A server-side error — a parse error, a timeout — ends at a terminal frame instead and keeps the connection. A CHAR holding a UTF-16 surrogate code unit comes back as that lone surrogate, which cannot be encoded to UTF-8. LONG[] columns are not supported on this path.

Every call returns the same iterator, so columns() and iter_rows() share one stream and an exhausted iterator stays empty. The batch being handed out is the only one held in memory. After cancel() no further rows are yielded, including any already decoded; after close() the iterator raises. A fresh iter_rows() call raises too once the stream ended with an error or its iterator was closed. A mid-query failover after iteration has begun raises QuestDBErrorCode.FailoverWouldDuplicate rather than replaying rows the caller already holds.

with db.query('select ts, symbol from trades limit 3') as result:
    for ts, symbol in result.iter_rows():
        print(ts, symbol)
to_arrow()

Read the full result into a pyarrow.Table. Requires pyarrow.

SYMBOL columns use uint32 dictionary indices, which Table.to_pandas() accepts only from pyarrow 23. For a pandas frame, call to_pandas() instead. DECIMAL columns with a precision of 18 or less arrive as decimal32 / decimal64, which need pyarrow 20 or later.

Materialise-whole: a mid-query failover replays the result transparently — the partial accumulation we hold is discarded from batch-0. The streaming path (__arrow_c_stream__, as consumed by polars.DataFrame(result) or pa.table(result)) does not replay: on a post-delivery failover, the consumer raises its own error, and the message contains [FailoverWouldDuplicate].

to_pandas(*, dtype_backend=None, types_mapper=None)

Read the full result into a pandas.DataFrame.

The default is a native (no pyarrow) hybrid built straight from the QWP column buffers: a nullable integer column with nulls becomes a pandas nullable Int* (pd.NA); without nulls it stays plain numpy. double/float stay numpy with NaN; SYMBOL → Categorical; TIMESTAMP → datetime64 (NaT); strings/decimal/uuid/binary → object. With no arguments or a dtype_backend, a GEOHASH column is a signed integer sized by its precision: int8 up to 7 bits, int16 to 15, int32 to 31, int64 to 60, as to_arrow() gives it. Analysis-safe (aggregations skip pd.NA/NaN), and feeds back into QuestDB.dataframe() for a type round-trip — the column kinds are carried in df.attrs['questdb'].

dtype_backend="pyarrow" / "numpy_nullable" / types_mapper select the pyarrow-backed path instead (pd.ArrowDtype, pandas nullable extension dtypes, or a custom mapper) — matching the pd.read_sql / pd.read_parquet convention. Those paths attach df.attrs['questdb'] too: a pandas dtype holds an Arrow type and no field, so attrs is what carries the IPV4 / CHAR / GEOHASH / UUID / LONG256 claims that live in Arrow field metadata. QuestDB.dataframe reads the claim back from either built-in backend — including the masked and object-dtype columns "numpy_nullable" produces for those five — so writing the result out again gives the same column types, and for those five the same wire bytes.

A custom types_mapper gets the same attrs, but whether the claim can be applied depends on the dtype the mapper picked. It has to be a shape that still holds the value: an Arrow-backed column, a NumPy uint32 / uint16 / integer column, or an object column of Python ints or bytes. Map one of those five columns to a float or a string dtype and the claim is ignored — the column lands as that dtype implies, and re-ingestion logs a questdb warning about the dropped claim.

df.attrs['questdb'] reads as an ordinary mapping but cannot be edited in place: pandas deep-copies the whole of attrs once per column whenever it propagates it, so a per-column claim that copied would make slicing and exporting quadratic in the column count. Every copy of the frame shares the one claim instead. To change what a frame claims, assign a new mapping to df.attrs['questdb'], or state the type outright with schema_overrides / symbols, which outrank it.

to_polars()

Read the full result into a polars.DataFrame. Requires polars and pyarrow.

Non-SYMBOL columns keep their exact polars.from_arrow dtypes (tz-aware Datetime, Decimal, Binary, List/Array, …). SYMBOL columns are built into a polars Categorical directly from their codes + dictionary through a persistent Categories registry — the wire code is its own physical categorical code, so there is no per-row Dictionary -> Categorical remap. Falls back to polars.from_arrow when polars’ (unstable) Categories API is unavailable. If that path fails, it also falls back and issues a RuntimeWarning: the values stay correct, but the read is slower. Under -W error that warning raises after the result was read, so the frame is lost; run the query again.

Materialise-whole: a mid-query failover replays the result transparently. This accumulates batches in-library (via pyarrow) so the partial result can be discarded on failover. The pyarrow-free polars.DataFrame(result) streams through __arrow_c_stream__ and does not replay: on a post-delivery failover, polars raises its own error, and the message contains [FailoverWouldDuplicate]. polars before 1.37.1 raises a PanicException, which except Exception does not catch.

class questdb.ExecDone(op_type, rows_affected)

A non-SELECT statement’s terminal EXEC_DONE report.

Returned by QuestDB.execute() / PooledReader.execute() and carried on QueryResult.exec_done; both give None instead for a statement that streamed a result set. A named tuple: done = db.execute(sql) reads as done.rows_affected.

op_type

The server’s QWP operation-type byte, carried through as an opaque int; its values are the server’s, not part of this client’s API.

rows_affected

The number of rows an INSERT or UPDATE touched; 0 for a statement that carries no count, such as one executed at parse time (TRUNCATE, RENAME TABLE, SET, …).

class questdb.ConnectionEvent(kind: ConnectionEventKind, host: str | None, port: str | None, previous_host: str | None, previous_port: str | None, attempt_number: int | None, cause_code: QuestDBErrorCode | None, cause_msg: str | None, timestamp_millis: int)

Bases: object

One connection-state transition, delivered to the connection_listener registered via QuestDB.from_conf().

Listeners run on a dedicated dispatcher thread — never on an I/O or caller thread — fed by a bounded inbox with a drop-oldest overflow policy, so a slow listener cannot stall ingest or reconnects. Successful events are queued only after negotiated connection state, including the server-advertised frame cap, is committed. They are not data-delivery or acknowledgement barriers. Exceptions raised by the listener are logged and swallowed.

__init__(kind: ConnectionEventKind, host: str | None, port: str | None, previous_host: str | None, previous_port: str | None, attempt_number: int | None, cause_code: QuestDBErrorCode | None, cause_msg: str | None, timestamp_millis: int) → None
attempt_number: int | None
cause_code: QuestDBErrorCode | None
cause_msg: str | None
host: str | None
kind: ConnectionEventKind
port: str | None
previous_host: str | None
previous_port: str | None
timestamp_millis: int
class questdb.ConnectionEventKind(*values)

Bases: TaggedEnum

Connection-state transitions observed by the ingress connection pool.

AllEndpointsUnreachable = ('all_endpoints_unreachable', 5)
AuthFailed = ('auth_failed', 6)
Connected = ('connected', 0)
Disconnected = ('disconnected', 1)
EndpointAttemptFailed = ('endpoint_attempt_failed', 4)
FailedOver = ('failed_over', 3)
Reconnected = ('reconnected', 2)
class questdb.ServerInfo(role: ServerRole, role_byte: int, epoch: int, capabilities: int, server_wall_ns: int, cluster_id: str, node_id: str, zone_id: str | None)

Bases: object

Snapshot of the server’s SERVER_INFO handshake, as advertised on the connection QuestDB.server_info() sampled. Fields mirror the Rust reader’s ServerInfo.

__init__(role: ServerRole, role_byte: int, epoch: int, capabilities: int, server_wall_ns: int, cluster_id: str, node_id: str, zone_id: str | None) → None
capabilities: int
cluster_id: str
epoch: int
node_id: str
role: ServerRole
role_byte: int
server_wall_ns: int
zone_id: str | None
class questdb.ServerRole(*values)

Bases: TaggedEnum

Cluster role advertised by the server’s SERVER_INFO handshake.

Other = ('other', 255)
Primary = ('primary', 1)
PrimaryCatchup = ('primary_catchup', 3)
Replica = ('replica', 2)
Standalone = ('standalone', 0)
class questdb.QuestDBError(code, msg, sender_error=None, *, in_doubt=False)

Bases: Exception

An error whilst using the QuestDB client.

__init__(code, msg, sender_error=None, *, in_doubt=False)
property code: QuestDBErrorCode

Return the error code.

property in_doubt: bool

Whether the failed ingestion operation may already have delivered its input. For QWP dataframe(), this covers the entire DataFrame, including earlier batches from the same call, even when a later batch fails local validation. For a sender flush, it describes that flush; it does not summarize earlier independent calls on the sender.

Retrying that input when this is true requires an appropriate application- or table-level deduplication guarantee. A false value says only that duplicate delivery is not known to be possible; a consumed one-shot input may still be impossible to replay.

property sender_error

Return the structured QWP/WebSocket diagnostic, if this error carries one from a QWP/WebSocket sender failure.

class questdb.QuestDBServerRejectionError(code, msg, sender_error=None, *, in_doubt=False)

Bases: QuestDBError

A terminal QWP/WebSocket server rejection.

The structured server payload is available through QuestDBError.sender_error.

class questdb.UnsupportedDataFrameShapeError(msg, column_failures=None)

Bases: QuestDBError

A DataFrame shape is not supported by the optimized columnar client path.

column_failures carries structured per-column rejection details where available; each is a dict with column (name or None for a whole-frame issue), target, source_code, and reason. The same per-column reasons are also folded into str(exc) so a bare print(exc) explains why the frame was rejected, and — when the designated timestamp is at fault — how to fix it on QuestDB.dataframe() (name a valid timestamp column, or pass ServerTimestamp).

__init__(msg, column_failures=None)
class questdb.QuestDBErrorCode(*values)

Bases: Enum

Category of Error.

ArrayError = 11
ArrowExport = 34
ArrowIngest = 16
ArrowUnsupportedColumnKind = 15
AuthError = 6
BadDataFrame = 65536
BatchTooLarge = 35
Cancelled = 30
ConfigError = 10
ConnectTimeout = 19
CouldNotResolveAddr = 0
DecimalError = 13
FailoverRetry = 17
FailoverWouldDuplicate = 31
HandshakeError = 20
HttpNotSupported = 8
InvalidApiCall = 1
InvalidBind = 23
InvalidName = 4
InvalidTimestamp = 5
InvalidUtf8 = 3
LimitExceeded = 28
NoSchema = 33
ProtocolError = 22
ProtocolVersionError = 12
QueryTimeout = 38
RoleMismatch = 18
SchemaDrift = 32
ServerFlushError = 9
ServerInternalError = 26
ServerLimitExceeded = 29
ServerParseError = 25
ServerRejection = 14
ServerSchemaMismatch = 24
ServerSecurityError = 27
SocketError = 2
StoreResendRequired = 36
SymbolDictFull = 37
TlsError = 7
UnsupportedServer = 21
__str__() → str

Return the name of the enum.

class questdb.SenderError(category: SenderErrorCategory, applied_policy: SenderErrorPolicy, status: int | None, message: str, message_sequence: int | None, from_fsn: int, to_fsn: int)

Bases: object

Structured QWP/WebSocket server diagnostic.

__init__(category: SenderErrorCategory, applied_policy: SenderErrorPolicy, status: int | None, message: str, message_sequence: int | None, from_fsn: int, to_fsn: int) → None
applied_policy: SenderErrorPolicy
category: SenderErrorCategory
from_fsn: int
message: str
message_sequence: int | None
status: int | None
to_fsn: int
class questdb.SenderErrorCategory(*values)

Bases: TaggedEnum

Category of a structured QWP/WebSocket diagnostic.

InternalError = ('internal_error', 2)
NotWritable = ('not_writable', 5)
ParseError = ('parse_error', 1)
ProtocolViolation = ('protocol_violation', 6)
SchemaMismatch = ('schema_mismatch', 0)
SecurityError = ('security_error', 3)
Unknown = ('unknown', 7)
WriteError = ('write_error', 4)
class questdb.SenderErrorPolicy(*values)

Bases: TaggedEnum

Applied policy for a structured QWP/WebSocket diagnostic.

Retriable = ('retriable', 0)
RetriableOther = ('retriable_other', 1)
Terminal = ('terminal', 2)
class questdb.QwpWsProgress(*values)

Bases: TaggedEnum

Progress mode for QWP/WebSocket senders.

Background = ('background', 0)
Manual = ('manual', 1)
class questdb.Protocol(*values)

Bases: TaggedEnum

Protocol to use for sending data to QuestDB.

See Which protocol? for more information.

Http = ('http', 2)
Https = ('https', 3)
Tcp = ('tcp', 0)
Tcps = ('tcps', 1)
Udp = ('udp', 4)
Ws = ('ws', 5)
Wss = ('wss', 6)
property tls_enabled
class questdb.TimestampMicros

Bases: object

A timestamp in microseconds since the UNIX epoch (UTC).

You may construct a TimestampMicros from an integer or a datetime.datetime, or simply call the TimestampMicros.now() method.

# Recommended way to get the current timestamp.
TimestampMicros.now()

# The above is equivalent to:
TimestampMicros(time.time_ns() // 1000)

# You can provide a numeric timestamp too. It can't be negative.
TimestampMicros(1657888365426838)

TimestampMicros can also be constructed from a datetime.datetime object.

TimestampMicros.from_datetime(
    datetime.datetime.now(tz=datetime.timezone.utc))

We recommend that when using datetime objects, you explicitly pass in the timezone to use. A datetime object without an associated timezone is interpreted as UTC (a UserWarning is emitted once per process). Note that datetime.datetime.now() is your local wall clock: use datetime.datetime.now(datetime.timezone.utc) or now() on this class for the current instant.

classmethod from_datetime(dt: datetime.datetime)

Construct a TimestampMicros from a datetime.datetime object.

classmethod now()

Construct a TimestampMicros from the current time as UTC.

value

Number of microseconds (Unix epoch timestamp, UTC).

class questdb.TimestampNanos

Bases: object

A timestamp in nanoseconds since the UNIX epoch (UTC).

You may construct a TimestampNanos from an integer or a datetime.datetime, or simply call the TimestampNanos.now() method.

# Recommended way to get the current timestamp.
TimestampNanos.now()

# The above is equivalent to:
TimestampNanos(time.time_ns())

# You can provide a numeric timestamp too. It can't be negative.
TimestampNanos(1657888365426838016)

TimestampNanos can also be constructed from a datetime object.

TimestampNanos.from_datetime(
    datetime.datetime.now(tz=datetime.timezone.utc))

We recommend that when using datetime objects, you explicitly pass in the timezone to use. A datetime object without an associated timezone is interpreted as UTC (a UserWarning is emitted once per process). Note that datetime.datetime.now() is your local wall clock: use datetime.datetime.now(datetime.timezone.utc) or now() on this class for the current instant.

classmethod from_datetime(dt: datetime.datetime)

Construct a TimestampNanos from a datetime.datetime object.

classmethod now()

Construct a TimestampNanos from the current time as UTC.

value

Number of nanoseconds (Unix epoch timestamp, UTC).

class questdb.Char

Bases: object

A QuestDB CHAR value stored as one UTF-16 code unit.

'\x00' is stored as code unit 0. CHAR has no physical NULL representation: QWP/Arrow egress returns it as 0, while text output renders it as empty. Some QuestDB SQL operations treat code unit 0 as CHAR’s null/absent marker.

value

The single Python code point represented by this CHAR.

class questdb.DateMillis

Bases: object

A QuestDB DATE value in milliseconds since the Unix epoch (UTC).

QuestDB DATE is a millisecond timestamp, not a civil date. The full signed 64-bit range is accepted, including pre-epoch values. INT64_MIN is accepted but reads back from QuestDB as NULL.

This is how Buffer.row writes a DATE column. DataFrames claim DATE from the column’s Arrow type instead — pa.timestamp('ms') (naive or tz-aware), pa.date32(), or pa.date64() — so there is no DATE cell type and no 'date' kind for schema_overrides. A NumPy datetime64[ms] dtype has no route of its own to DATE and widens to a microsecond TIMESTAMP, unless the frame carries a df.attrs['questdb'] claim naming the column DATE, which puts the Arrow type back on it first. That claim is written as {'kind': 'date'} under the column’s entry in the version-1 claim mapping. Like the other QWP-only types, DATE needs a QWP sender; ILP senders have no DATE type, so the datetime columns they accept all land as TIMESTAMP.

classmethod from_datetime(dt: datetime.datetime)

Construct a DateMillis by flooring a datetime to milliseconds.

classmethod now()

Construct a DateMillis from the current time as UTC.

value

Milliseconds since the Unix epoch (UTC).

class questdb.Long256

Bases: object

An unsigned 256-bit integer for a QuestDB LONG256 column.

The value whose four 64-bit limbs are all 0x8000000000000000 is accepted but reads back from QuestDB as NULL.

value

The unsigned integer value.

class questdb.Geohash

Bases: object

A QuestDB GEOHASH value represented by bits and precision.

This scalar wrapper is strict: precision must be in 1..=60 and bits must satisfy 0 <= bits < 2**precision. Bulk GEOHASH columns, whether NumPy, Arrow or object, deliberately have a different, unchecked value contract; see QuestDB.dataframe.

Precision is pinned per column within one buffer’s worth of rows. The first GEOHASH cell written to a column fixes the precision for the rest of that batch, and a later cell at a different precision is rejected by Buffer.row itself, with the buffer rewound to what it held before that row – the bad row is not left waiting for a flush.

The pin goes with the buffer: a flush clears it and the next batch starts over. A precision the server’s column does not have is therefore the server’s to reject, at flush time.

bits

The packed geohash bits.

classmethod from_string(value)

Construct a GEOHASH from one to twelve base32 characters.

precision

The precision in bits.

class questdb.TlsCa(*values)

Bases: TaggedEnum

Verification mechanism for the server’s certificate.

Here webpki refers to the WebPKI library and os refers to the operating system’s certificate store.

See TLS for more information.

OsRoots = ('os_roots', 1)
PemFile = ('pem_file', 3)
WebpkiAndOsRoots = ('webpki_and_os_roots', 2)
WebpkiRoots = ('webpki_roots', 0)
class questdb.ServerTimestampType

Bases: object

A placeholder value to indicate that the data should be inserted using a server-generated-timestamp.

Don’t instantiate this class directly, use the singleton ServerTimestamp instead.

This feature is mostly provided for legacy compatibility. We recommend always specifying an explicit timestamp.

Using ServerTimestamp will prevent QuestDB’s deduplication feature from working as it would generate unique rows on resubmission.

questdb.ServerTimestamp

A placeholder value to indicate that the data should be inserted using a server-generated-timestamp.

Don’t instantiate this class directly, use the singleton ServerTimestamp instead.

This feature is mostly provided for legacy compatibility. We recommend always specifying an explicit timestamp.

Using ServerTimestamp will prevent QuestDB’s deduplication feature from working as it would generate unique rows on resubmission.

class questdb._client.TaggedEnum(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: Enum

Base class for tagged enums.

property c_value
classmethod parse(tag)

Parse from the tag name.

property tag

Short name.

questdb.WARN_HIGH_RECONNECTS

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

questdb.ingress (legacy)

The deprecated 4.x compatibility shim. New code imports from questdb.

class questdb.ingress.Buffer

Internal row-serialization buffer, managed by Sender.

Kept importable as questdb.ingress.Buffer for legacy ILP/HTTP and ILP/TCP code that constructs buffers explicitly and flushes them via sender.flush(buffer).

__init__(*args, **kwargs)
capacity() → int

The current buffer capacity.

clear()

Reset the buffer.

Note that flushing a buffer will (unless otherwise specified) also automatically clear it.

This method is designed to be called only in conjunction with sender.flush(buffer, clear=False).

Raises QuestDBError (InvalidApiCall) if a Buffer.row or Buffer.dataframe call on this buffer is still in progress.

dataframe(df, *, table_name: str | None = None, table_name_col: None | int | str = None, symbols: str | bool | List[int] | List[str] = 'auto', at: ServerTimestampType | int | str | TimestampNanos | datetime.datetime)

Add a pandas DataFrame to the buffer.

Also see Sender.dataframe if you are not using the buffer explicitly. Over ILP and QWP/UDP it accepts the arguments shown here and adds auto-flushing. Over QWP/WebSocket it instead uses the direct columnar path, additionally accepting max_rows_per_batch and schema_overrides; see QuestDB.dataframe for that path’s supported column types and arguments.

Requires pandas and numpy. pyarrow is only needed when the frame contains pd.ArrowDtype / pd.Categorical / string dtype columns — purely NumPy / object dtypes work without it.

Adding a dataframe can trigger auto-flushing behaviour, even between rows of the same dataframe. To avoid this, you can use HTTP and transactions (see Sender.transaction).

Parameters:
  • df (pandas.DataFrame) – The pandas DataFrame to serialize to the buffer.

  • table_name (str or None) –

    The name of the table to which the rows belong.

    If None, the table name is taken from the table_name_col parameter. If both table_name and table_name_col are None, the table name is taken from the DataFrame’s index name (df.index.name attribute).

  • table_name_col (str or int or None) –

    The name or index of the column in the DataFrame that contains the table name.

    If None, the table name is taken from the table_name parameter. If both table_name and table_name_col are None, the table name is taken from the DataFrame’s index name (df.index.name attribute).

    If table_name_col is an integer, it is interpreted as the index of the column starting from 0. The index of the column can be negative, in which case it is interpreted as an offset from the end of the DataFrame. E.g. -1 is the last column.

  • symbols (str or bool or list[str] or list[int]) –

    The columns to be serialized as symbols.

    If 'auto' (default), all columns of dtype 'categorical' are serialized as symbols. If True, all str columns are serialized as symbols. If False, no columns are serialized as symbols.

    The list of symbols can also be specified explicitly as a list of column names (str) or indices (int). Integer indices start at 0 and can be negative, offset from the end of the DataFrame. E.g. -1 is the last column.

    Only columns containing strings can be serialized as symbols.

  • at (TimestampNanos, datetime.datetime, int or str or None) –

    The designated timestamp of the rows.

    You can specify a single value for all rows or column name or index. If ServerTimestamp, timestamp is assigned by the server for all rows. To pass in a timestamp explicitly as an integer use the TimestampNanos wrapper type. To get the current timestamp, use TimestampNanos.now(). When passing a datetime.datetime object, the timestamp is converted to nanoseconds. A naive datetime object is interpreted as UTC — never your machine’s local timezone — and a UserWarning is emitted once per process (call datetime.datetime.now(tz=datetime.timezone.utc) for the current timestamp to avoid bugs).

    To specify a different timestamp for each row, pass in a column name (str) or index (int, 0-based index, negative index supported): In this case, the column needs to be of dtype datetime64[ns] (assumed to be in the UTC timezone and not local, due to differences in Pandas and Python datetime handling) or datetime64[ns, tz]. When a timezone is specified in the column, it is converted to UTC automatically.

    A timestamp column can also contain None values. The server will assign the current timestamp to those rows.

    Note: All timestamps are always converted to nanoseconds and in the UTC timezone. Timezone information is dropped before sending and QuestDB will not store any timezone information.

Note: It is an error to specify both table_name and table_name_col.

Note: The “index” column of the DataFrame is never serialized, even if it is named.

Example:

import pandas as pd
import questdb as qi

buf = qi.ingress.Buffer(protocol_version=2)
# ...

df = pd.DataFrame({
    'location': ['London', 'Managua', 'London'],
    'temperature': [24.5, 35.0, 25.5],
    'humidity': [0.5, 0.6, 0.45],
    'ts': pd.date_range('2021-07-01', periods=3)})
buf.dataframe(
    df, table_name='weather', at='ts', symbols=['location'])

# ...
sender.flush(buf)

Pandas to ILP datatype mappings

Pandas Mappings

Pandas dtype

Nulls

ILP Datatype

'bool'

N

BOOLEAN

'boolean'

N α

BOOLEAN

'object' (bool objects)

N α

BOOLEAN

'uint8'

N

INTEGER

'int8'

N

INTEGER

'uint16'

N

INTEGER

'int16'

N

INTEGER

'uint32'

N

INTEGER

'int32'

N

INTEGER

'uint64'

N

INTEGER β

'int64'

N

INTEGER

'UInt8'

Y

INTEGER

'Int8'

Y

INTEGER

'UInt16'

Y

INTEGER

'Int16'

Y

INTEGER

'UInt32'

Y

INTEGER

'Int32'

Y

INTEGER

'UInt64'

Y

INTEGER β

'Int64'

Y

INTEGER

'object' (int objects)

Y

INTEGER β

'float32' γ

Y (NaN)

FLOAT

'float64'

Y (NaN)

FLOAT

'object' (float objects)

Y (NaN)

FLOAT

'string' (str objects)

Y

STRING (default), SYMBOL via symbols arg. δ

'string[pyarrow]'

Y

STRING (default), SYMBOL via symbols arg. δ

'category' (str objects) ε

Y

SYMBOL (default), STRING via symbols arg. δ

'object' (str objects)

Y

STRING (default), SYMBOL via symbols arg. δ

'datetime64[ns]'

Y

TIMESTAMP ζ

'datetime64[ns, tz]'

Y

TIMESTAMP ζ

'object' (Decimal objects)

Y (NaN)

DECIMAL

Note

  • α: Note some pandas dtypes allow nulls (e.g. 'boolean'), where the QuestDB database does not.

  • β: The valid range for integer values is -2^63 to 2^63-1. Any 'uint64', 'UInt64' or python int object values outside this range will raise an error during serialization.

  • γ: Upcast to 64-bit float during serialization.

  • δ: Columns containing strings can also be used to specify the table name. See table_name_col.

  • ε: We only support categories containing strings. If the category contains non-string values, an error will be raised.

  • ζ: The ‘.dataframe()’ method only supports datetimes with nanosecond precision. The designated timestamp column (see at parameter) maintains the nanosecond precision, whilst values stored as columns have their precision truncated to microseconds. All dates are sent as UTC and any additional timezone information is dropped. If no timezone is specified, we follow the pandas convention of assuming the timezone is UTC. Datetimes before 1970-01-01 00:00:00 UTC are not supported. If a datetime value is specified as None (NaT), it is interpreted as the current QuestDB server time set on receipt of message.

Error Handling and Recovery

In case an exception is raised during dataframe serialization, the buffer is left in its previous state. The buffer remains in a valid state and can be used for further calls even after an error.

For clarification, as an example, if an invalid None value appears at the 3rd row for a bool column, neither the 3rd nor the preceding rows are added to the buffer.

Note: This differs from the Sender.dataframe method, which modifies this guarantee due to its auto_flush logic.

Performance Considerations

The Python GIL is released during serialization if it is not needed. If any column requires the GIL, the entire serialization is done whilst holding the GIL.

Column types that require the GIL are:

  • Columns of str, float or int or float Python objects.

  • The 'string[python]' dtype.

init_buf_size

The initial capacity of the buffer when first created.

This may grow over time, see capacity().

max_name_len

Maximum length of a table or column name.

reserve(additional)

Ensure the buffer has at least additional bytes of future capacity.

Parameters:

additional (int) – Additional bytes to reserve.

row(table_name: str, *, symbols: Dict[str, str | None] | None = None, columns: Dict[str, None | bool | int | float | str | TimestampMicros | TimestampNanos | datetime.datetime | numpy.ndarray | Decimal | uuid.UUID | ipaddress.IPv4Address | bytes | bytearray | memoryview | Char | DateMillis | Long256 | Geohash] | None = None, at: ServerTimestampType | TimestampNanos | datetime.datetime)

Add a single row (line) to the buffer.

# All fields specified.
buffer.row(
    'table_name',
    symbols={'sym1': 'abc', 'sym2': 'def', 'sym3': None},
    columns={
        'col1': True,
        'col2': 123,
        'col3': 3.14,
        'col4': 'xyz',
        'col5': TimestampMicros(123456789),
        'col6': datetime(2019, 1, 1, 12, 0, 0),
        'col7': numpy.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]),
        'col8': None},
    at=TimestampNanos(123456789))

# Only symbols specified. Designated timestamp assigned by the db.
buffer.row(
    'table_name',
    symbols={'sym1': 'abc', 'sym2': 'def'}, at=Server.Timestamp)

# Float columns and timestamp specified as `datetime.datetime`.
# Pay special attention to the timezone, which if unspecified is
# interpreted as UTC.
buffer.row(
    'sensor data',
    columns={
        'temperature': 24.5,
        'humidity': 0.5},
    at=datetime.datetime.now(tz=datetime.timezone.utc))

Python strings passed as values to symbols are going to be encoded as the SYMBOL type in QuestDB, whilst Python strings passed as values to columns are going to be encoded as the STRING type.

Refer to the QuestDB documentation to understand the difference between the SYMBOL and STRING types (TL;DR: symbols are interned strings).

Column values can be specified with Python types directly and map as so:

Python type

Serialized as ILP type

bool

BOOLEAN

decimal

DECIMAL

int

INTEGER

float

FLOAT

str

STRING

numpy.ndarray

ARRAY

datetime.datetime and TimestampMicros

TIMESTAMP

uuid.UUID

UUID (QWP-only)

ipaddress.IPv4Address

IPV4 (QWP-only)

bytes, bytearray, or memoryview

BINARY (QWP-only)

Char

CHAR (QWP-only)

DateMillis

DATE (QWP-only)

Long256

LONG256 (QWP-only)

Geohash

GEOHASH (QWP-only)

None

Column is skipped and not serialized.

Note: Support for NumPy arrays (numpy.array) requires QuestDB server version 9.0.0 or higher.

The seven QWP-only types require protocol udp, ws, or wss and are rejected by ILP buffers used by tcp, tcps, http, and https. They require QuestDB 10 or newer, the first production QWP implementation.

QuestDB reserves these values as NULL sentinels, but the client deliberately accepts them: IPV4 0.0.0.0, DATE INT64_MIN, UUID 80000000-0000-0000-8000-000000000000, and a LONG256 whose four 64-bit limbs are all 0x8000000000000000. CHAR has no physical NULL sentinel: '\x00' is stored as code unit 0, although some SQL operations treat it as CHAR’s null/absent marker. GEOHASH has no sentinel collision. Empty BINARY b'' is a real empty value and is distinct from NULL.

A bare int is a 64-bit LONG, so a value outside -2**63 .. 2**63-1 raises OverflowError. On a QWP buffer the message names Long256, the wrapper that sends it as a 256-bit LONG256 instead; an ILP buffer, which has no LONG256 to offer, reports the bare conversion failure.

If the destination table was already created, then the columns types will be cast to the types of the existing columns whenever possible (Refer to the QuestDB documentation pages linked above).

Adding a row can trigger auto-flushing behaviour.

Parameters:
  • table_name – The name of the table to which the row belongs.

  • symbols – A dictionary of symbol column names to str values. As a convenience, you can also pass a None value which will have the same effect as skipping the key: If the column already existed, it will be recorded as NULL, otherwise it will not be created.

  • columns – A dictionary mapping column names to the supported column values listed above. As a convenience, you can also pass a None value which will have the same effect as skipping the key: If the column already existed, it will be recorded as NULL, otherwise it will not be created.

  • at – The timestamp of the row. This is required! If ServerTimestamp, timestamp is assigned by QuestDB. If datetime, the timestamp is converted to nanoseconds. A nanosecond unix epoch timestamp can be passed explicitly as a TimestampNanos object.