DuckDB Extension
Tributary
Read Apache Kafka topics from DuckDB SQL. Inspect messages, snapshot a topic into a table, and join the results with local data.
On this page
Tributary is the right tool today for reading Kafka topics from DuckDB — snapshots, joins, and cluster inspection all work. A more capable successor is in active development that will add producer support, Avro and Protobuf Schema Registry support, and transactional semantics. Subscribe below and we'll email you the moment it ships, with a migration note.
Read Kafka messages as rows
Apache Kafka stores streams of messages in topics. Tributary lets a DuckDB query consume records from a topic, inspect their contents, and save or join them using SQL.
Use it to investigate messages, take a bounded snapshot, or combine a topic’s data with reference tables. You supply the broker, topic, and consumer configuration, including authentication where required. Reading a topic with a query does not automatically keep a local table synchronized with future messages.
Example
Read a Kafka topic
Change the topic and broker to match your Kafka cluster. The returned columns describe the messages in that topic.
Before you run it: Provide a reachable Kafka broker and an existing topic. The local address below assumes you already have Kafka running.
Run this example in the DuckDB CLI or a native client.
INSTALL tributary FROM community;
LOAD tributary;
SELECT *
FROM tributary_scan_topic(
'test-topic', "bootstrap.servers" := 'localhost:9092'
)
LIMIT 10;
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY IF NOT EXISTS query_farm
WITH PREFIX 'https://duckdb-extensions.query.farm';
INSTALL tributary FROM query_farm;
LOAD tributary FROM query_farm;
SELECT *
FROM tributary_scan_topic(
'test-topic', "bootstrap.servers" := 'localhost:9092'
)
LIMIT 10;
Rows depend on the topic. SQL LIMIT caps displayed rows; review scan settings for offsets, partitions, and consumption behavior before using a large or active topic.
Common tasks
Deep Dive
Technical Details
DuckDB ↔ Apache Kafka
SQL access to Apache Kafka topics — consumer-side, partition-parallel, librdkafka-configured.
What you can do with one query
The shortest path from “Kafka topic” to “queryable DuckDB table”:
CREATE OR REPLACE TABLE orders_snapshot ASSELECT partition, "offset", json_extract_string(decode(message), '$.user_id') AS user_id, json_extract_string(decode(message), '$.amount') AS amountFROM tributary_scan_topic('orders', "bootstrap.servers" := 'kafka:9092');tributary_scan_topic reads each partition in parallel, hands DuckDB the message bytes as a BLOB, and lets you decode them inline with whatever SQL fits your serialization. librdkafka’s client configuration options — security.protocol, sasl.username, ssl.ca.location, client.id — pass through as named parameters.
This extension is the consumer side of Kafka — read paths, snapshot paths, cluster introspection, plus a Schema Registry client. It is not a Kafka producer: there is no write-back function in the current version. For publishing to topics, keep a real Kafka producer.
Status is experimental; the function surface may change as more librdkafka paths get exposed. Option names are checked when the query is bound — a misspelled name is a Binder Error: Invalid named parameter — while option values go to librdkafka unchanged, so a bad value surfaces as a librdkafka error when the scan starts.
Architecture
Tributary is a thin DuckDB table-function wrapper around librdkafka, the standard C/C++ Kafka client. Every named parameter you pass to tributary_scan_topic or tributary_metadata is forwarded to librdkafka’s configuration as-is — there’s no Tributary-specific naming layer to learn. The accepted names are librdkafka’s global (client) properties from its CONFIGURATION reference, plus schema.registry.url. Topic-level properties such as auto.offset.reset are not accepted.
Each scan reads every non-empty partition from its first retained offset up to the high watermark it saw when the scan started, so the result is a finite snapshot of the topic even while producers keep writing. Partitions are separate scan tasks that DuckDB’s threads pick up in parallel. The scan doesn’t join a consumer group or commit offsets, so it never moves an application’s consumer-group position.
The output schema is (topic VARCHAR, partition INTEGER, offset BIGINT, key BLOB, message BLOB); a message without a key has a NULL key. The key and message columns are intentionally BLOB — Tributary doesn’t try to guess your wire format. Turn UTF-8 payloads into text with decode(message) and use DuckDB’s JSON functions, or pair with the avro / protobuf community extensions, depending on what’s on your topic. Don’t use message::VARCHAR for JSON: in DuckDB 2.0 a BLOB to VARCHAR cast escapes quotes and non-ASCII bytes ({\x22user_id\x22: ...}), which JSON functions reject. When the topic’s schemas live in a Schema Registry, Tributary can decode JSON-schema payloads for you — see Schema Registry.
Securing credentials
Authentication parameters are plain SQL values, so anything passed inline appears in the query text. Keep them in a tributary_cluster secret, which stores librdkafka options for one cluster and redacts the sensitive ones (sasl.username, sasl.password, ssl.key.password, key and certificate locations) in duckdb_secrets(). The secret’s scope is a kafka:// name, and a scan picks the secret up when its topic is written as kafka://<name>/<topic>:
CREATE SECRET prod_kafka ( TYPE tributary_cluster, SCOPE 'kafka://prod', "bootstrap.servers" 'broker.example.com:9093', "security.protocol" 'SASL_SSL', "sasl.mechanism" 'SCRAM-SHA-512', "sasl.username" getenv('KAFKA_USER'), "sasl.password" getenv('KAFKA_PASSWORD'));
SELECT * FROM tributary_scan_topic('kafka://prod/orders');Named parameters passed to the scan override the secret’s values. A plain topic name (no kafka:// prefix) never matches a secret. tributary_metadata doesn’t read secrets; pass its connection options as named parameters.
Without a secret, source credentials from the environment and pass them with getvariable:
SET VARIABLE k_user = getenv('KAFKA_USER');SET VARIABLE k_pass = getenv('KAFKA_PASSWORD');
SELECT *FROM tributary_scan_topic('orders', "bootstrap.servers" := 'broker.example.com:9093', "security.protocol" := 'SASL_SSL', "sasl.mechanism" := 'SCRAM-SHA-512', "sasl.username" := getvariable('k_user'), "sasl.password" := getvariable('k_pass'));See Kafka security: SASL and SSL for the upstream guidance on which mechanism fits your cluster.
Scoping the scan
tributary_scan_topic always reads everything the topic currently retains — fine for a low-volume topic or a one-off snapshot, but expensive on a high-throughput production topic. There is no offset or time range to pass, and a WHERE on partition or offset is applied after the messages are read, so it narrows the result but not the amount consumed. To keep the cost down:
- Check the size first with
tributary_metadata— partition counts tell you how wide the scan will be. - Scan once, query many times. Materialize the topic with
CREATE TABLE ... AS SELECT * FROM tributary_scan_topic(...)and run the exploratory queries against the local table. - Keep retention in mind. The scan starts at the oldest retained offset, so a topic’s retention settings decide how far back it reaches.
Schema Registry
Tributary includes a client for a Confluent-compatible Schema Registry. Every function takes the registry URL as its first argument; credentials come from a tributary_schema_registry secret whose scope is that URL (basic_auth_username + basic_auth_password, or bearer_access_token):
CREATE SECRET registry ( TYPE tributary_schema_registry, SCOPE 'https://registry.example.com', basic_auth_username 'svc-analytics', basic_auth_password getenv('REGISTRY_PASSWORD'));
SELECT * FROM tributary_schema_registry_subjects('https://registry.example.com', false);SELECT * FROM tributary_schema_registry_get_schema_latest('https://registry.example.com', 'orders-value');- Browse:
tributary_schema_registry_subjects,tributary_schema_registry_versions, and theget_schema_*lookups by latest version, version number, subject and id, or GUID. - Change:
tributary_schema_registry_register_schema,tributary_schema_registry_delete_subject,tributary_schema_registry_delete_subject_version. - Decode while scanning: pass
"schema.registry.url" := '...'totributary_scan_topic(or put it in thetributary_clustersecret). Tributary looks up the latest schema of the<topic>-keyand<topic>-valuesubjects; when one is a JSON schema, that column comes back decoded asJSON— namedkeyandvalue— instead of aBLOB. A subject that doesn’t exist leaves its column as aBLOB. Only JSON schemas are supported: an Avro or Protobuf subject is an error. - Encode:
tributary_schema_registry_serialize_jsonencodes a JSON value with a JSON schema into the Schema Registry wire format for a topic, as aBLOBready to hand to a producer.
Compared to alternatives
- Kafka Connect — purpose-built for sustained, structured Kafka↔system pipelines. Reach for that when you need delivery guarantees, Avro / Protobuf schema handling, or recurring sync. Reach for Tributary when you want SQL on demand.
kcat+ CSV → DuckDB — what people often did before this extension. Tributary collapses the file-in-the-middle and lets you push DuckDB predicates back into the consume.- Python
confluent-kafka+ DataFrames — much heavier setup; you give up DuckDB’s vectorized execution against the streamed data and re-introduce row-format conversion at the DataFrame boundary.
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Type | Description |
|---|---|---|
|
Inspection
Inspect cluster shape and the loaded extension build — broker / topic / partition discovery via |
||
| tributary_metadata() | Object type: Table function | Inspect a Kafka cluster without consuming from a specific topic. |
| tributary_version() | Object type: Scalar function | Returns the loaded Tributary extension version as a date-stamped build string (e.g. |
|
Reading
Stream messages from Kafka topics into DuckDB tables. Multi-partition topics are scanned in parallel; the result is |
||
| tributary_scan_topic() | Object type: Table function | Read messages from a Kafka topic as DuckDB rows. |
|
Schema Registry
Browse and change a Confluent-compatible Schema Registry — subjects, versions and schemas — and encode JSON values in its wire format. Credentials come from a |
||
| tributary_schema_registry_delete_subject() | Object type: Table function | Delete a subject and return the versions that were deleted. |
| tributary_schema_registry_delete_subject_version() | Object type: Table function | Delete one version of a subject. |
| tributary_schema_registry_get_schema_by_guid() | Object type: Table function | Fetch a schema by its GUID. |
| tributary_schema_registry_get_schema_by_subject_and_id() | Object type: Table function | Fetch a schema by subject and schema id. |
| tributary_schema_registry_get_schema_by_version() | Object type: Table function | Fetch one version of a subject's schema. |
| tributary_schema_registry_get_schema_latest() | Object type: Table function | Fetch the latest schema registered under a subject. |
| tributary_schema_registry_get_schema_latest_json_structure() | Object type: Scalar function |
Convert the latest JSON schema of a subject into a DuckDB JSON structure description, for use with json_transform / from_json.
|
| tributary_schema_registry_register_schema() | Object type: Table function | Register a schema under a subject and return the registration. |
| tributary_schema_registry_serialize_json() | Object type: Scalar function |
Encode a JSON value with a JSON schema into the Schema Registry wire format, as a BLOB ready to hand to a Kafka producer.
|
| tributary_schema_registry_subjects() | Object type: Table function | List the registry's subjects, one row per subject. |
| tributary_schema_registry_versions() | Object type: Table function | List the version numbers registered under a subject. |
|
Streaming / Kafka
|
||
| tributary_cluster | Object type: Secret | librdkafka options for one Kafka cluster. |
| tributary_schema_registry | Object type: Secret | Credentials for a Schema Registry. |
No extension contents match that search.
API Reference
Function Reference
Security
Secrets
DuckDB secrets for storing the credentials and keys used by the tributary extension.
tributary_cluster
Description
librdkafka options for one Kafka cluster. The scope must start with kafka://; tributary_scan_topic uses the secret when its topic is written kafka://<name>/<topic>. Named parameters passed to the scan override the secret's values. Credential and key options are redacted in duckdb_secrets().
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| Parameter bootstrap.servers | Type VARCHAR | Required Optional |
Description
Comma-separated host:port broker list.
|
| Parameter security.protocol | Type VARCHAR | Required Optional |
Description
PLAINTEXT, SSL, SASL_PLAINTEXT, or SASL_SSL.
|
| Parameter sasl.mechanism | Type VARCHAR | Required Optional |
Description
SASL mechanism, e.g. SCRAM-SHA-512.
|
| Parameter sasl.username | Type VARCHAR | Required Optional | Description SASL username (redacted). |
| Parameter sasl.password | Type VARCHAR | Required Optional | Description SASL password (redacted). |
| Parameter schema.registry.url | Type VARCHAR | Required Optional | Description Schema Registry URL used to decode JSON-schema topics. |
| Parameter ... | Type VARCHAR | Required Optional |
Description
Any other librdkafka global (client) property accepted by tributary_scan_topic.
|
Examples
SASL_SSL cluster
CREATE SECRET prod_kafka (
TYPE tributary_cluster,
SCOPE 'kafka://prod',
"bootstrap.servers" 'broker.example.com:9093',
"security.protocol" 'SASL_SSL',
"sasl.mechanism" 'SCRAM-SHA-512',
"sasl.username" getenv('KAFKA_USER'),
"sasl.password" getenv('KAFKA_PASSWORD')
);
tributary_schema_registry
Description
Credentials for a Schema Registry. The scope is the registry URL; every tributary_schema_registry_* function and a scan with schema.registry.url look the secret up by that URL. Basic auth is used when both basic-auth values are set, otherwise the bearer token.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| Parameter basic_auth_username | Type VARCHAR | Required Optional | Description Basic-auth user name. |
| Parameter basic_auth_password | Type VARCHAR | Required Optional | Description Basic-auth password (redacted). |
| Parameter bearer_access_token | Type VARCHAR | Required Optional | Description Bearer token (redacted). |
Examples
Registry with basic auth
CREATE SECRET registry (
TYPE tributary_schema_registry,
SCOPE 'https://registry.example.com',
basic_auth_username 'svc-analytics',
basic_auth_password getenv('REGISTRY_PASSWORD')
);
Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
Recipes for Apache Kafka read paths, JSON decoding, joins, and discovery.
Read a Kafka topic
The simplest pattern — point a query at a topic and treat it as a table:
SELECT *FROM tributary_scan_topic('orders', "bootstrap.servers" := 'kafka:9092');Each row corresponds to one Kafka message, with metadata columns (topic, partition, offset) plus the raw key and payload (message) as BLOBs. The scan reads everything the topic retains, up to where each partition ended when the scan started. See tributary_scan_topic for the full signature and parameter table.
Decode JSON messages on the way in
Most topics carry JSON. Turn the message bytes into text with decode and use DuckDB’s JSON functions:
SELECT partition, "offset", json_extract_string(decode(message), '$.user_id') AS user_id, json_extract_string(decode(message), '$.event') AS eventFROM tributary_scan_topic('events', "bootstrap.servers" := 'kafka:9092');offset is a reserved keyword in DuckDB (used by LIMIT … OFFSET …), so quote it as "offset" whenever you reference the column directly.
Use decode(message) rather than message::VARCHAR: in DuckDB 2.0 casting a BLOB to VARCHAR escapes quotes and non-ASCII bytes ({\x22user_id\x22: ...}), which the JSON functions reject.
For Avro / Protobuf payloads, pair with DuckDB’s avro / protobuf community extensions and decode the BLOB column the same way.
Filter a scan
Filter on the metadata columns like any other table:
SELECT *FROM tributary_scan_topic('orders', "bootstrap.servers" := 'kafka:9092')WHERE partition = 0 AND "offset" >= 1000000;The filter runs after the messages are read: every partition is still consumed from its first retained offset, so this narrows the result, not the cost of the scan. For time-bounded slices, decode the timestamp from the message body and filter on it. If you’ll query the same slice repeatedly, snapshot it into a table first (below).
Snapshot a topic into a local table
CREATE OR REPLACE TABLE orders_snapshot ASSELECT *FROM tributary_scan_topic('orders', "bootstrap.servers" := 'kafka:9092');Useful for ad-hoc analytics over a finite slice of the stream — once it’s a DuckDB table, the rest of your query plan runs at full vectorized speed.
Join Kafka with local data
SELECT k.partition, k."offset", u.name, msg.eventFROM tributary_scan_topic('events', "bootstrap.servers" := 'kafka:9092') kCROSS JOIN LATERAL ( SELECT json_extract_string(decode(k.message), '$.user_id') AS user_id, json_extract_string(decode(k.message), '$.event') AS event) msgJOIN users u ON u.user_id = msg.user_id::BIGINT;The classic enrichment pattern — Kafka stream on one side, reference data on the other, joined in a single SQL statement.
Authenticate against managed Kafka
Source credentials from the environment so they don’t appear in cached query plans:
CREATE SECRET prod_kafka ( TYPE tributary_cluster, SCOPE 'kafka://prod', "bootstrap.servers" 'broker.example.com:9093', "security.protocol" 'SASL_SSL', "sasl.mechanism" 'SCRAM-SHA-512', "sasl.username" getenv('KAFKA_USER'), "sasl.password" getenv('KAFKA_PASSWORD'));
-- The kafka://prod/ prefix picks up the secretSELECT * FROM tributary_scan_topic('kafka://prod/orders');The tributary_cluster secret holds the librdkafka options for one cluster and redacts the credentials in duckdb_secrets(). Named parameters passed to the scan override the secret’s values. To skip the secret, pass the same options as named parameters, using getvariable('k_user') for values loaded with SET VARIABLE k_user = getenv('KAFKA_USER').
Same pattern for SSL-only clusters: pass security.protocol := 'SSL' plus the relevant ssl.ca.location / ssl.certificate.location parameters. See Kafka SASL and SSL for the upstream config matrix.
Discover what’s on a cluster
-- List every topic the cluster exposesSELECT unnest(topics).name AS topic_nameFROM tributary_metadata("bootstrap.servers" := 'kafka:9092');-- Count partitions per topicSELECT t.name AS topic, array_length(t.partitions) AS partitionsFROM ( SELECT unnest(topics) AS t FROM tributary_metadata("bootstrap.servers" := 'kafka:9092'))ORDER BY partitions DESC;See tributary_metadata for the full output shape.
Decode JSON-schema topics through a Schema Registry
When producers write JSON-schema payloads through a Confluent-compatible Schema Registry, point the scan at the registry and the key and value come back as JSON:
SELECT partition, "offset", value->>'user_id' AS user_idFROM tributary_scan_topic('orders', "bootstrap.servers" := 'kafka:9092', "schema.registry.url" := 'https://registry.example.com');Tributary uses the latest schema of the orders-key and orders-value subjects; a missing subject leaves that column as a BLOB named key or message. Only JSON schemas are decoded — an Avro or Protobuf subject is an error. Browse the registry itself with the tributary_schema_registry_* functions:
SELECT * FROM tributary_schema_registry_subjects('https://registry.example.com', false);SELECT version, schema_type, schemaFROM tributary_schema_registry_get_schema_latest('https://registry.example.com', 'orders-value');Registry credentials go in a tributary_schema_registry secret scoped to the registry URL. See Schema Registry in the technical details.
Diagnostics
SELECT tributary_version(); -- e.g. '20251020.01'Use tributary_version when you need to confirm the loaded build against bug reports or release notes.
Platform Support
Compatibility
Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.
Quick Facts
Platforms
DuckDB 2.0.0
- Linux x86_64 aarch64
- Linux (musl) Not available
- macOS Apple Silicon
- Windows x86_64
- WASM Not available
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | x86_64 | 9.98 MB |
| Linux | aarch64 | 9.32 MB |
| macOS | Apple Silicon | 6.81 MB |
| Windows | x86_64 | 13.84 MB |
Compressed download sizes for the release shown above.
DuckDB & Haybarn
Release calendar- DuckDB v2.0.0 Query.Farm repository Supported
Browser examples use Haybarn-Wasm 1.5.6.
Checked .