open source · python

Catch the spike
before it pages you.

Time-series anomaly detection and alerting with a dbt-like project layout. A metric is a SQL query plus a detector in YAML — run it with one command.

api_error_rate last 24h · 5min
metric expected range anomaly recovery
Anomaly · api_error_rate
value 4.2 · expected ≤ 1.1 → Slack
Recovered · api_error_rate
value 1.0 · back within range → Slack
Works with
ClickHouse PostgreSQL MySQL MariaDB DuckDB MotherDuck Snowflakehybrid BigQueryhybrid
Alerts to
Slack Mattermost Telegram Email Webhook Discord Teams Google Chat ntfy
quickstart

From nothing to a caught anomaly
— on one local file.

DuckDB runs in-process — zero servers, zero credentials. Three small files, one command, and --report renders the caught anomaly and the alerts it fires into a self-contained HTML file. Wire a channel (ntfy needs no token) when you want the notification delivered live.

quickstart — bash
$ pip install "detectkit[duckdb]"
Successfully installed detectkit · duckdb · numpy · pydantic
# 3 small files — no server, no account, no credentials:
# detectkit_project.yml · profiles.yml (duckdb) · metrics/signups.yml
# the metric builds its own series in SQL, with one injected dip.
$ dtk run --select signups --report
┌─ LOAD └─ Loaded 213 datapoints
┌─ DETECT · mad └─ Detected 8 anomalies
┌─ ALERT (replayed into the report — nothing dispatched)
✓ Pipeline completed successfully
│ Report → reports/signups.html (8 anomalies, 8 alerts)
# open the report: the dip, the expected band, and the alerts it
# fires — fully offline. Add a channel to deliver it on a live run.

Runs on a fresh machine with nothing to provision — the full project is in examples/quickstart-duckdb. Swap the profile for ClickHouse, Postgres, MySQL or a hybrid warehouse when you're ready — the four commands don't change. DuckDB guide →

dtk init-claude

AI-native: build metrics with an assistant, out of the box.

One command sets up Claude Code for your project folder — a CLAUDE.md, a .claude/rules/detectkit/ reference, and five skills: dtk-setup-project (configure your database), dtk-new-metric (scaffold a metric), dtk-tune (dial in a detector by hand in an interactive browser cockpit — with autotune built in), dtk-autotune (search for the best detector automatically), and dtk-feedback (file a redacted bug report or feature request upstream). Now an assistant writes metrics, tunes detectors, wires up alerts, and reports issues with full knowledge of detectkit. Re-run it after an upgrade to refresh the context.

dtk init-claude
Target: ~/monitoring
┌─ CLAUDE.md
└─ detectkit section created
┌─ .claude/rules/detectkit/
│ alerting.md (created)
│ autotune.md (created)
│ cli.md (created)
│ detectors.md (created)
│ metrics.md (created)
│ overview.md (created)
└─ project.md (created)
┌─ .claude/skills/
│ dtk-autotune/SKILL.md (created)
│ dtk-feedback/SKILL.md (created)
│ dtk-new-metric/SKILL.md (created)
│ dtk-setup-project/SKILL.md (created)
└─ dtk-tune/SKILL.md (created)
Done. Claude context ready (13 created).
dtk mcp · read-only

Ask your pipeline what fired — and why.

dtk mcp is a strictly read-only Model Context Protocol server over your _dtk_* state. Point Claude Code, Claude Desktop or any MCP client at it, and your assistant answers questions from the real pipeline — no writes, no dashboards, no data leaving your infra. MCP guide →

Your AI assistant dtk mcp · read-only
Which metrics fired this week — and why?
called list_metrics replay_alerts query_detections
Three metrics fired in the last 7 days:
signups down · recovered
Dropped to 812 vs expected ≥ 1,150 on Jul 14, 12:00. mad, 6 intervals; back in range by 13:00.
checkout_error_rate up · still open
4.2% vs ≤ 1.1%. autoreg flagged a broken daily shape — wrong for the curve, not just the level.
daily_active_users no-data
No rows for 2h on Jul 12 — likely an upstream ETL gap, not a real drop.
Read live from project state — 10 read-only tools, zero write paths. The chat lives in your MCP client; detectkit ships the server, not a UI.
configuration

From a SQL query to a caught anomaly.

A metric is just a query plus a detector in YAML. dtk run handles the corridor, the quorum and the alert — nothing else to wire up.

metrics/api_errors.yml YAML
# metrics/api_errors.yml
name: api_error_rate
interval: "5min"
query: |
SELECT
toStartOfInterval(timestamp, INTERVAL 5 MINUTE) AS timestamp,
countIf(status_code >= 500) / count() * 100 AS value
FROM http_requests
WHERE timestamp >= '{{ dtk_start_time }}'
AND timestamp < '{{ dtk_end_time }}'
GROUP BY timestamp ORDER BY timestamp
detectors:
- type: mad
params:
threshold: 3.0
window_size: 2016 # 7d of 5-min points
window_weights: exponential
half_life: "1d"
alerting:
enabled: true
channels: [mattermost_ops]
consecutive_anomalies: 3
direction: "up"
mentions: [oncall_engineer, here]
$ dtk run --select api_error_rate
LOAD 12 points · resumed
DETECT 1 anomaly · detector mad
ALERT ✓ sent to mattermost_ops
api_error_rate anomaly
value 4.2 · expected ≤ 1.1 · severity 3.40
✓ pipeline completed 1 metric · 1 detector · 0.4s idempotent · resumable
SQL on your warehouse detector in YAML one command version-controlled
dtk run

A load → detect → alert run, in one tree.

The real output of dtk run — a load → detect → alert tree with cyan step headers and colored status lines. Idempotent: it resumes from the last saved point.

LOAD

Run the SQL on your warehouse, in batches, from the last checkpoint.

DETECT

Each detector scores points against its learned corridor of normal.

ALERT

Quorum met → post to chat with the rule up top, recovery on the way back.

dtk run --select api_error_rate
Project root: ~/monitoring
Found 1 metric(s) to process
Processing metric: api_error_rate
Config file: metrics/api_errors.yml
Steps: load, detect, alert
┌─ LOAD
│ Resuming from last saved: 2026-06-19 11:55:00
│ Loading from 2026-06-19 12:00:00 to 2026-06-19 12:05:00
│ Total points: ~1 | Batch size: 10,000
│ Loading in single batch...
└─ Loaded 1 datapoints
┌─ DETECT
│ Running 1 detector(s)...
│ [1/1] Detector: mad
│ Detecting from 2026-06-19 12:00:00 to 2026-06-19 12:05:00
│ Total points: ~1 | Batch size: 1,000
│ └─ Detected 1 anomalies
└─ Total anomalies: 1
┌─ ALERT
│ Checking alert conditions...
⚠ Alert triggered! Sending to 1 channel(s)...
mattermost_ops
└─ Sent 1/1 alerts
✓ Pipeline completed successfully
detectors

Robust statistics, not magic.

Every detector learns a corridor of normal from recent history, then flags the moment a metric steps outside it. Switch the detector to see the kind of metric it's built for.

metric expected range anomaly
anomaly · 3.40 σ
7 days ago now
mad
Median absolute deviation

Measures the typical distance from the median. A handful of wild spikes barely move it — the most robust default.

Corridor
median ± 3 × MAD
Best for
Spiky, noisy metrics with outliers in their history.
metric expected range anomaly
anomaly · z = 4.1
7 days ago now
zscore
Z-score

Classic mean ± k standard deviations. Fast and simple, but one big outlier inflates the band — keep it for clean data.

Corridor
mean ± 3 × σ
Best for
Clean, roughly bell-shaped metrics.
metric expected range anomaly
anomaly · beyond fence
7 days ago now
iqr
Interquartile range

Builds the corridor from the middle 50% of values, then extends fences 1.5×IQR out. Comfortable with skewed, long-tailed data.

Corridor
[ Q1 − 1.5·IQR , Q3 + 1.5·IQR ]
Best for
Skewed distributions and one-sided outliers.
metric expected range anomaly
anomaly · 6.9 σ_r
7 days ago now
autoreg
Autoregressive forecast

Predicts each point from the few just before it, then flags what misses the forecast. Catches values that are normal in absolute terms but wrong for the shape of the curve — dips, stalls, broken rhythm.

Corridor
ŷ ± 3 × σ_residual
Best for
Smooth, strongly-patterned metrics where dynamics matter, not just level.
metric expected range anomaly
anomaly · > max
max min
7 days ago now
manual_bounds
Manual bounds

No statistics at all — you set hard floor and ceiling values. Alerts the instant a metric crosses a known SLA line.

Corridor
value < min or value > max
Best for
Known SLAs and hard business limits.

// the statistical corridors are recomputed per window with seasonality grouping & recency weighting — newer points count more; autoreg refits its forecast per point
// each detector is shown on the metric shape it handles best — robust, bell-shaped, skewed, pattern-driven or hard-bounded
// opt-in stabilization keeps a sustained incident from widening its own corridor and masking its own tail

try it · live

Play with the real tuning cockpit.

The playground is dtk tune — the same cockpit the tool ships — running on a synthetic metric you shape in your browser. Turn the detector's real knobs, watch the corridor of normal and what gets flagged, confirm alerts, mark incidents, and read the live catch-rate / false-alert metrics — nothing is sent anywhere.

Open the live cockpit → the real dtk tune · mad / zscore / iqr / autoreg / manual · tune / review / label · live metrics
dtk tune · cockpit

Dial in a detector on your real data.

dtk tune --select <metric> opens a browser cockpit on the metric's real history. Turn a knob and the corridor recomputes live, with catch-rate and false-alert metrics pinned over the chart. Four modes: Tune the band, Review fired alerts (one click — valid or false alarm; a confirmed alert becomes ground truth), Label incidents (drag a span, lasso the anomaly cloud), and Autotune — the real search engine, run server-side on exactly the window you're looking at. Apply validates the config and writes it back into the metric YAML, archiving the previous version to metrics/.history/.

payments_errors

// recompute is client-side — the same detector math as the pipeline; no lock, nothing persists until Apply
// labels save to incidents/<metric>/ — the ground truth dtk autotune optimizes against, so a labeling round here feeds the next search

See the tuning cockpit guide →

dtk ui · project cockpit

The whole project on one screen.

dtk ui opens a localhost cockpit over the project: every metric's alert frequency and freshness at a glance (quality chips once incidents are labeled), the full HTML report one click away, run / autotune / tune driven as real dtk subprocesses — and a built-in editor to create, edit and delete metric YAMLs, validated before any write, previous versions archived to metrics/.history/.

See dtk ui → overview · pipeline control · metric editor
alerting

Alerts that lead with the rule that fired.

Direction-aware multi-detector quorum, cooldown, recovery and no-data alerts — posted to chat with the alert and its rule up top, anomaly evidence below. A slow burn that never strings N anomalies in a row? An opt-in window-share rule (anomaly_window + min_anomaly_share) catches it without paging on every blip.

The same alert, posted by detectkit to each channel — rendered as that channel formats it: one folding attachment on Slack/Mattermost (its verbose tail collapses behind “Show more”), escaped HTML on Telegram, a branded card in email, an inline field grid on Discord (embeds don't fold), an Adaptive Card on Microsoft Teams (posted under the flow's own identity — no detectkit branding), a Cards v2 card on Google Chat, and a push notification on ntfy. A generic webhook (with an attachments payload Rocket.Chat accepts as-is, plus json / alertmanager formats) covers everything else. Each leads with the project name ([payments]) so several projects can share one channel while keeping the brand bot identity. The dashboard_url below becomes a first-class link on every channel.

detectkitAPP12:04
@oncall_engineer @here
🔴 [payments] Alert: api_error_rate
Anomalous for 30m — 6 consecutive 5min intervals.
Rule min_detectors=1 · direction=up · consecutive=3
Value 4.2
Expected <= 1.1
Quorum 1/1 · above
Severity 3.40
Anomaly began 2026-06-19 11:39:00 (Europe/Moscow)
Latest reading 2026-06-19 12:04:00 (Europe/Moscow)
Detectors mad
Parameters {"threshold": 3.0, "window_size": 2016, "half_life": "1d"}
detectkitAPP12:04
@oncall_engineer @here
🟢 [payments] Alert cleared: api_error_rate
The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals).
Rule min_detectors=1 · direction=up · consecutive=3
Value 1.0
Expected <= 1.1
Anomaly began 2026-06-19 11:36:00 (Europe/Moscow)
Alert fired 2026-06-19 11:46:00 (Europe/Moscow)
Recovered 2026-06-19 12:36:00 (Europe/Moscow)
Detectors mad
detectkitBOT12:04
@oncall_engineer @here
🔴 [payments] Alert: api_error_rate
Anomalous for 30m — 6 consecutive 5min intervals.
Rule min_detectors=1 · direction=up · consecutive=3
Value 4.2
Expected <= 1.1
Quorum 1/1 · above
Severity 3.40
Anomaly began 2026-06-19 11:39:00 (Europe/Moscow)
Latest reading 2026-06-19 12:04:00 (Europe/Moscow)
Detectors mad
Parameters {"threshold": 3.0, "window_size": 2016, "half_life": "1d"}
detectkitBOT12:04
@oncall_engineer @here
🟢 [payments] Alert cleared: api_error_rate
The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals).
Rule min_detectors=1 · direction=up · consecutive=3
Value 1.0
Expected <= 1.1
Anomaly began 2026-06-19 11:36:00 (Europe/Moscow)
Alert fired 2026-06-19 11:46:00 (Europe/Moscow)
Recovered 2026-06-19 12:36:00 (Europe/Moscow)
Detectors mad
detectkit
🔴 [payments] Anomaly · api_error_rate Anomalous for 30m — 6 consecutive 5min intervals. Rule min_detectors=1 · direction=up · consecutive=3 • Value: 4.2 · Expected: <= 1.1 • Quorum: 1/1 · above • Severity: 3.40 • Anomaly began: 2026-06-19 11:39:00 (Europe/Moscow) · Latest reading: 2026-06-19 12:04:00 (Europe/Moscow) • Detector: mad • Parameters: {"threshold": 3.0, "window_size": 2016, "half_life": "1d"} Open dashboard · How to read this alert @oncall_engineer
12:04
detectkit
🟢 [payments] Recovered · api_error_rate The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals). Rule min_detectors=1 · direction=up · consecutive=3 • Value: 1.0 · Expected: <= 1.1 • Anomaly began: 2026-06-19 11:36:00 (Europe/Moscow) · Alert fired: 2026-06-19 11:46:00 (Europe/Moscow) · Recovered: 2026-06-19 12:36:00 (Europe/Moscow) • Detector: mad Open dashboard · How to read this alert @oncall_engineer
12:04
detectkitBOTToday at 12:04
🔴 [payments] Alert: api_error_rate
Anomalous for 30m — 6 consecutive 5min intervals.
Rule min_detectors=1 · direction=up · consecutive=3
Value 4.2 · Expected <= 1.1
Parameters
{"threshold": 3.0, "window_size": 2016, "half_life": "1d"}
Quorum
1/1 · above
Severity
3.40
Anomaly began
2026-06-19 11:39:00 (Europe/Moscow)
Latest reading
2026-06-19 12:04:00 (Europe/Moscow)
Detectors
mad
detectkitBOTToday at 12:04
🟢 [payments] Alert cleared: api_error_rate
The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals).
Rule min_detectors=1 · direction=up · consecutive=3
Value 1.0 · Expected <= 1.1
Anomaly began
2026-06-19 11:36:00 (Europe/Moscow)
Alert fired
2026-06-19 11:46:00 (Europe/Moscow)
Recovered
2026-06-19 12:36:00 (Europe/Moscow)
Detectors
mad
WorkflowsAPP12:04
🔴 [payments] Alert: api_error_rate
Anomalous for 30m — 6 consecutive 5min intervals.
Rule: min_detectors=1 · direction=up · consecutive=3
Value
4.2
Expected
<= 1.1
Quorum
1/1 · above
Severity
3.40
Anomaly began
2026-06-19 11:39:00 (Europe/Moscow)
Latest reading
2026-06-19 12:04:00 (Europe/Moscow)
Detectors
mad
{"threshold": 3.0, "window_size": 2016, "half_life": "1d"}
detectkit · payments
WorkflowsAPP12:04
🟢 [payments] Alert cleared: api_error_rate
The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals).
Rule: min_detectors=1 · direction=up · consecutive=3
Value
1.0
Expected
<= 1.1
Anomaly began
2026-06-19 11:36:00 (Europe/Moscow)
Alert fired
2026-06-19 11:46:00 (Europe/Moscow)
Recovered
2026-06-19 12:36:00 (Europe/Moscow)
Detectors
mad
detectkit · payments
🔴 [payments] Alert: api_error_rate
detectkit · payments
Anomalous for 30m — 6 consecutive 5min intervals.
Rule min_detectors=1 · direction=up · consecutive=3
Value
4.2
Expected
<= 1.1
Quorum
1/1 · above
Severity
3.40
Anomaly began
2026-06-19 11:39:00 (Europe/Moscow)
Latest reading
2026-06-19 12:04:00 (Europe/Moscow)
Detectors
mad
Parameters
{"threshold": 3.0, "window_size": 2016, "half_life": "1d"}
🟢 [payments] Alert cleared: api_error_rate
detectkit · payments
The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals).
Rule min_detectors=1 · direction=up · consecutive=3
Value
1.0
Expected
<= 1.1
Anomaly began
2026-06-19 11:36:00 (Europe/Moscow)
Alert fired
2026-06-19 11:46:00 (Europe/Moscow)
Recovered
2026-06-19 12:36:00 (Europe/Moscow)
Detectors
mad
ntfynow
🚨 [payments] Alert: api_error_rate
Anomalous for 30m — 6 consecutive 5min intervals. Rule: min_detectors=1 · direction=up · consecutive=3 Value: 4.2 | Expected: <= 1.1 Quorum: 1/1 · above Severity: 3.40 Anomaly began: 2026-06-19 11:39:00 (Europe/Moscow) | Latest reading: 2026-06-19 12:04:00 (Europe/Moscow) Detectors: mad Parameters: {"threshold": 3.0, "window_size": 2016, "half_life": "1d"} Dashboard: https://grafana.ops/d/api-errors How to read this alert: https://dtk.pipelab.dev/guides/reading-alerts/ @oncall_engineer
Priority 4 · high
ntfynow
✅ [payments] Alert cleared: api_error_rate
The alert condition no longer holds — the metric is back within expected bounds. Incident lasted 1h (12 consecutive 5min intervals). Rule: min_detectors=1 · direction=up · consecutive=3 Value: 1.0 | Expected: <= 1.1 Anomaly began: 2026-06-19 11:36:00 (Europe/Moscow) | Alert fired: 2026-06-19 11:46:00 (Europe/Moscow) | Recovered: 2026-06-19 12:36:00 (Europe/Moscow) Detectors: mad Dashboard: https://grafana.ops/d/api-errors How to read this alert: https://dtk.pipelab.dev/guides/reading-alerts/ @oncall_engineer
Priority 3 · default
alerting:
  channels: [mattermost_ops]
  dashboard_url: https://grafana.ops/d/api-errors   # one line → a link on every channel
hybrid mode

Your warehouse runs the SQL. A free local DB keeps the state.

Warehouses punish frequent small writes — Snowflake bills a 60-second minimum every time it resumes; BigQuery a 10 MiB minimum per query. So detectkit runs a metric's load SQL on your warehouse, then keeps every _dtk_* table — detections, alert state, resume cursors — in a cheap local database. You monitor the warehouse without paying warehouse prices for the bookkeeping. Hybrid mode →

Source · your warehouse
Snowflake · BigQuery · ClickHouse · …
runs the metric's load SQL
detectkit
detect · alert · resume
no state written to the warehouse
State · a cheap local DB
DuckDB · Postgres · MySQL
all _dtk_* tables live here
# metrics/revenue.yml — one line flips a metric into hybrid mode
source_profile: snowflake_prod   # load runs here; state stays on your --profile
where it fits

What detectkit is — and isn't.

Deliberately narrow: statistical anomaly detection and alerting for business metrics you define in SQL. Here's how that differs from the neighbors — by scope and deployment, the things that actually vary.

Not a data-observability platform — no lineage, catalog or schema-drift claims.
SaaS observability
data-quality clouds — Monte Carlo, Anomalo, Metaplane
themWhole-warehouse platforms at $18k–$200k+/yr; monitoring runs in their cloud.
detectkitFree, self-hosted, scoped to the metrics you pick — data never leaves your infra.
Prometheus / Grafana
infra telemetry + static thresholds
themBuilt for infra metrics; the anomaly add-ons (Grafana ML, vmanomaly) are paid.
detectkitStatistical detection over arbitrary SQL business metrics — and it speaks Alertmanager's webhook.
dbt tests / Soda
build-time data-quality assertions
themPass/fail checks at build time — is this value null, unique, in range?
detectkitModels the metric's time series — seasonality, trend, recency. Runs alongside your tests.
TS detector libraries
detector toolkits — Darts, Merlion
themDetector code you wire up, schedule and operate yourself.
detectkitThe whole pipeline around the detector — scheduling, state, alerting, a UI and tuning.

Compared on scope + deployment only — never maturity, support or star counts. The OSS corner of this space is thin: most alternatives are either heavy platforms or unmaintained.

NAB-benchmarked detectors MIT licensed numpy-only core · no pandas your data never leaves your infra live stars + downloads, omitted if a source is down

Ship your first detector in five minutes.

SQL + YAML, one command. No agents, no dashboards to babysit.