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.

Talk to Sales

Tell us about your team and we'll show you how Dench fits your workflow.

By submitting this form you agree to be contacted by our team. See our Privacy Policy and Terms of Service.

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