5.0 Migration Guide¶
Connect once, then stream, load, or query¶
The QWP/WebSocket API has one connection-owning root, the QuestDB handle returned by questdb.connect():
QuestDB.senderlends a pooled row-building sender — a lean lease exposingrow(),dataframe(),flush(),wait(), the FSN delivery receipts (flush_and_get_fsn/await_acked_fsn), rejection polling (poll_error) andclose(), not aSenderinstance (isinstancechecks and type annotations must not assume theSenderclass — use the exportedquestdb.PooledSenderinstead);QuestDB.dataframebulk-loads a whole DataFrame through the direct columnar path;QuestDB.queryexecutes queries; andQuestDB.readerlends a pooledquestdb.PooledReaderfor running several queries on one connection.
Existing row code can move from a standalone Sender to a pooled one:
import questdb
from questdb import Sender, ServerTimestamp
# Before: a standalone multi-transport sender.
with Sender.from_conf('ws::addr=localhost:9000;') as sender:
sender.row('weather', columns={'temperature': 21.5},
at=ServerTimestamp)
sender.flush()
# After: the handle owns the pool and lends a row builder.
with questdb.connect('ws::addr=localhost:9000;') as db:
with db.sender() as sender:
sender.row('weather', columns={'temperature': 21.5},
at=ServerTimestamp)
sender.flush(wait=True)
The pooled sender publishes into the store-and-forward QWP path. A plain
flush() returns after local acceptance; flush(wait=True) also waits
for the server’s OK acknowledgement of everything published through the
lease. The wait is a pure ack barrier — only a terminal connection failure
raises. Server rejections are pushed to the pool’s error_handler
(a questdb.connect() keyword; one questdb.SenderError per
rejection, on a dedicated dispatcher thread). Without a handler every
rejection is logged through the questdb logger — ERROR for terminal
rejections, WARNING for retriable ones, which the queue replays — so
rejections are never silent. To ingest concurrently, borrow one sender per
thread.
DataFrame bulk loads over QWP/WebSocket¶
Over ws:: / wss::, DataFrame bulk loads use the direct columnar
path — a database operation, not stream serialization. The recommended
entry point is QuestDB.dataframe on the
handle; the sender variants exist as conveniences and share the same path:
import pandas as pd
import questdb
from questdb import Sender, ServerTimestamp
frame = pd.DataFrame({
't': [21.5, 20.9],
'ts': pd.to_datetime(['2026-01-01T00:00:00Z', '2026-01-01T00:00:01Z'])})
with questdb.connect('ws::addr=localhost:9000;') as db:
# Recommended — on the handle:
db.dataframe(frame, table_name='weather', at='ts')
# Convenience: on a pooled sender — same path, direct connection
# from the pool; NOT part of the sender's row stream:
with db.sender() as sender:
sender.row('weather', columns={'t': 21.5}, at=ServerTimestamp)
sender.dataframe(frame, table_name='weather', at='ts')
sender.flush()
# Convenience: on a standalone sender — same path, poolless direct
# connection:
with Sender.from_conf('ws::addr=localhost:9000;') as sender:
sender.dataframe(frame, table_name='weather', at='ts')
All commit the whole source before returning, are independent of sf_dir,
and re-send from your DataFrame on transient failures (raising with
in_doubt set when a blind re-send could duplicate rows); none converts
the dataframe into row calls. dataframe() opens a direct connection just
for that call (borrowed from the pool for a pooled sender, opened from the
sender’s own configuration — carrying its auth/TLS — for a standalone one)
and commits immediately — it has no ordering relationship with rows
buffered on a sender via row() and does not flush them; publish those
with flush(). This works for any standalone ws sender, whether built
via Sender.from_conf / Sender.from_env or the Sender(...)
constructor.
Over ILP/HTTP, ILP/TCP and QWP/UDP, sender.dataframe() is unchanged and
fully supported (over UDP it serializes row by row into fire-and-forget
datagrams, with the same delivery caveats as row()).
Update imports from questdb.ingress¶
The 4.x questdb.ingress module is now a deprecated compatibility shim.
It keeps ILP/HTTP and ILP/TCP code running — including IngressError /
IngressErrorCode aliases and explicit Buffer construction with
sender.flush(buffer) — and emits one DeprecationWarning at import.
New code imports from the top-level package:
# Before
from questdb.ingress import Sender, IngressError
# After
from questdb import Sender, QuestDBError
Buffer is not part of the top-level API: buffers are managed internally
by senders. Where 4.x code built buffers on worker threads and flushed them
through one sender, borrow one pooled sender per thread instead.
Behavioural changes to watch for¶
Broader
except IngressError.IngressErroris an alias of the unifiedQuestDBError, so handlers written for 4.x ingestion errors now also catch query, pool and egress failures raised through the same handle. Narrow byerr.codewhere that matters.BadDataFrame.valuechanged. TheBadDataFrameerror code’s numeric.valuemoved from 14 to0x10000; raw value 14 now belongs toServerRejection. Codes 0–13 are unchanged. Compare by member identity (err.code is QuestDBErrorCode.BadDataFrame), never by integer.Stricter timestamp handling. Pre-epoch
datetimescalars now floor toward negative infinity instead of truncating toward zero (4.x encoded fractional-second pre-epoch instants one second too high);TimestampNanos.from_datetimeraisesValueErrorfor datetimes outside the 64-bit nanosecond range instead of silently overflowing; and negative sub-seconddatetime.timedeltadurations (e.g.timedelta(milliseconds=-500)) raiseValueErrorinstead of being mis-read as positive.Naive datetimes are UTC. Every path — DataFrame column cells, scalar
at=,row()values,from_datetime, query binds — interprets naive (timezone-unaware) values as UTC. 4.x interpreted naive scalars in the process-local timezone; if your values were local wall-clock times, convert withdt.astimezone(); if they were already UTC, usedt.replace(tzinfo=timezone.utc)(or just silence the one-timeUserWarning).datetime.now()callers should switch toTimestampNanos.now()ordatetime.now(timezone.utc).