DuckDB for Real-Time Analytics: What's Possible

DuckDB isn't a streaming database, but it can power near-real-time analytics. Here's what's actually possible and how to architect it correctly.

Mark Rachapoom
Mark Rachapoom
·6 min read
DuckDB for Real-Time Analytics: What's Possible

Let's be direct: DuckDB is not a streaming database. If you need sub-second analytics on data arriving at millions of events per second, you need ClickHouse, Apache Druid, or Apache Flink.

But for most businesses, "real-time" means something more like "last 5 minutes" or "updated every minute." For that use case, DuckDB is more than capable — and dramatically simpler to operate.

What "Real-Time" Actually Means for Most Businesses#

RequirementWhat Teams Usually MeanWhat They Actually Need
"Real-time sales data"Updated every few minutes5-minute refresh is fine
"Live dashboard"Reads whenever someone opens itQuery runs in <2 seconds
"Real-time alerts"Notified within minutesCron job every minute works
"Streaming events"Every click tracked instantlyBatch insert every 30 seconds

Genuine real-time streaming analytics (sub-100ms latency from event to dashboard) is rare and expensive. Most teams asking for "real-time" analytics are actually fine with near-real-time.

DuckDB's Refresh Rate#

DuckDB can comfortably support analytics that refresh on a 1-60 second cycle:

  • 1 second: Reads only — no problem. Query a read-only DuckDB file in <100ms.
  • 5 seconds: Lightweight writes + reads — insert a batch of events, query aggregates.
  • 30 seconds: Standard batch pipeline — collect events, batch insert, serve analytics.
  • 60 seconds: Solid production pattern for most dashboards.

For dashboards that need to show "current state" to users, this is entirely acceptable.

Architecture: Near-Real-Time with DuckDB#

Pattern 1: Append-and-Query#

Event producers → Event queue (Redis/SQS) → Batch consumer → DuckDB → Dashboard

The batch consumer runs every 10-30 seconds:

import duckdb
import redis
import time
 
con = duckdb.connect('analytics.duckdb')
r = redis.Redis()
 
while True:
    # Pop batch of events from queue
    batch = []
    for _ in range(1000):  # Process up to 1000 events
        event = r.lpop('events')
        if not event:
            break
        batch.append(json.loads(event))
    
    if batch:
        # Batch insert into DuckDB
        con.executemany("""
            INSERT INTO events (user_id, event_type, value, occurred_at)
            VALUES (?, ?, ?, ?)
        """, [(e['user_id'], e['type'], e['value'], e['ts']) for e in batch])
    
    time.sleep(10)

Dashboard queries run against DuckDB directly:

-- Last hour activity
SELECT 
    DATE_TRUNC('minute', occurred_at) AS minute,
    COUNT(*) AS events,
    COUNT(DISTINCT user_id) AS active_users
FROM events
WHERE occurred_at > NOW() - INTERVAL '1 hour'
GROUP BY minute
ORDER BY minute;

Pattern 2: File-Based Refresh#

For lighter workloads, write events to Parquet files and let DuckDB query them:

import pandas as pd
import duckdb
from datetime import datetime
 
# Every 30 seconds: flush events buffer to a new Parquet file
def flush_events(events_buffer):
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    df = pd.DataFrame(events_buffer)
    df.to_parquet(f'events/batch_{timestamp}.parquet')
    events_buffer.clear()
-- Query all recent files
SELECT 
    DATE_TRUNC('minute', occurred_at) AS minute,
    COUNT(*) AS events
FROM read_parquet('events/batch_*.parquet')
WHERE occurred_at > NOW() - INTERVAL '1 hour'
GROUP BY minute
ORDER BY minute;

DuckDB auto-discovers all matching Parquet files and queries them together. New files appear in the next query automatically.

Pattern 3: Hot/Cold Separation#

Keep recent data in a "hot" table for fast queries, archive older data to Parquet:

-- Hot table: last 24 hours
CREATE TABLE events_hot AS 
SELECT * FROM events WHERE occurred_at > NOW() - INTERVAL '24 hours';
 
-- Cold archive: everything older (as Parquet)
COPY (
    SELECT * FROM events WHERE occurred_at <= NOW() - INTERVAL '24 hours'
) TO 'events_archive.parquet' (FORMAT PARQUET);
 
-- Query combining both
SELECT * FROM events_hot
UNION ALL
SELECT * FROM read_parquet('events_archive.parquet')
WHERE occurred_at > '2026-01-01';

This keeps the hot table small and fast while the cold archive accumulates without growing the DuckDB file.

Live Dashboards with DuckDB#

Here's a pattern for a live dashboard that updates every 30 seconds:

// Frontend: poll every 30 seconds
async function refreshDashboard() {
    const response = await fetch('/api/analytics/current');
    const data = await response.json();
    updateCharts(data);
}
 
setInterval(refreshDashboard, 30000);
refreshDashboard(); // Initial load
# Backend: query DuckDB on demand
from flask import Flask, jsonify
import duckdb
 
app = Flask(__name__)
con = duckdb.connect('analytics.duckdb', read_only=True)
 
@app.route('/api/analytics/current')
def current_analytics():
    result = con.execute("""
        SELECT 
            COUNT(*) AS events_last_hour,
            COUNT(DISTINCT user_id) AS active_users,
            SUM(CASE WHEN event_type = 'purchase' THEN value ELSE 0 END) AS revenue_today
        FROM events
        WHERE occurred_at > NOW() - INTERVAL '1 hour'
    """).fetchone()
    
    return jsonify({
        'events': result[0],
        'active_users': result[1],
        'revenue': float(result[2])
    })

This serves dashboard data from DuckDB with query times typically under 200ms for well-indexed tables.

Real-Time Alerts#

Cron jobs can implement alerts without a streaming infrastructure:

#!/usr/bin/env python3
# alert_check.py - run every minute via cron
 
import duckdb
import requests
 
con = duckdb.connect('analytics.duckdb', read_only=True)
 
# Check for error rate spike
error_rate = con.execute("""
    SELECT 
        COUNT(CASE WHEN event_type = 'error' THEN 1 END) * 100.0 / COUNT(*) AS error_rate
    FROM events
    WHERE occurred_at > NOW() - INTERVAL '5 minutes'
""").fetchone()[0]
 
if error_rate > 5.0:  # Alert if error rate > 5%
    requests.post('https://hooks.slack.com/services/...', json={
        'text': f'⚠️ Error rate spike: {error_rate:.1f}% in last 5 minutes'
    })
# crontab: run every minute
* * * * * /path/to/alert_check.py

DenchClaw's Approach#

DenchClaw uses a near-real-time pattern for its CRM analytics. When you ask the agent for pipeline metrics, it queries DuckDB directly — the data is always fresh because DenchClaw writes to DuckDB synchronously as you create/update entries.

For the DenchClaw App Builder, you can create dashboards that auto-refresh:

// DenchClaw app with auto-refresh
async function loadMetrics() {
    const metrics = await dench.db.query(`
        SELECT 
            "Stage",
            COUNT(*) AS deals,
            SUM("Deal Value") AS value
        FROM v_deals
        WHERE "Status" = 'Active'
        GROUP BY "Stage"
    `);
    updatePipelineChart(metrics);
}
 
// Refresh every 60 seconds
setInterval(loadMetrics, 60000);
loadMetrics();

The widget mode in DenchClaw apps supports configurable auto-refresh intervals — exactly the near-real-time analytics pattern.

When You Actually Need a Streaming Database#

If any of these are true, move beyond DuckDB to a streaming solution:

  • Sub-second latency from event to dashboard is a hard requirement
  • Event volume exceeds 100,000 events/second
  • You need continuous SQL queries (like Kafka Streams or Flink)
  • You need windowed stream processing with out-of-order event handling

For these cases, look at ClickHouse (fast ingestion + analytics), Apache Druid (native streaming + OLAP), or Materialize (PostgreSQL-compatible streaming SQL).

Frequently Asked Questions#

Can DuckDB read from Kafka?#

Not directly. Use a Kafka consumer to batch-write to DuckDB periodically, or write to S3 Parquet files and query with httpfs.

What's the minimum refresh interval DuckDB can reliably support?#

For read-only queries: sub-second. For writes followed by reads: depends on write volume, but 5-10 seconds is generally safe for batch inserts under a few thousand rows.

Can DuckDB handle 1,000 writes per second?#

Single-process: yes, with batch inserts. Multi-process concurrent writes: no — DuckDB has a single-writer constraint.

How do I minimize DuckDB latency for dashboards?#

Use read-only connections for dashboards (doesn't block the writer), add indexes on timestamp columns, and keep the hot data period short.

Is DuckDB suitable for IoT real-time analytics?#

Not for high-frequency IoT (10,000+ events/second). For moderate IoT (batch every 30 seconds), DuckDB works well.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Related articles

Keep reading

View all