Schedule icon
Query icon
If icon
Switch icon
Log icon
SlackIncomingWebhook icon

ClickHouse Slow Query Threshold Gate

Audit ClickHouse slow and long-running queries via system.query_log and system.processes, alert on risk, and optionally kill one.

Categories
DataInfrastructure

A query that takes ten seconds instead of one hundred milliseconds does not throw an error - it just quietly eats CPU, memory, and concurrency slots until enough of them pile up to degrade everything else on the cluster. This blueprint reads ClickHouse's own system.query_log for queries that have already finished slowly, and system.processes for queries that are still running past a time budget, classifies the cluster into a three-state risk level, alerts on anything but a clean bill of health, and can terminate the single longest-running active query with ClickHouse's own KILL QUERY statement, behind two independent gates.

How it works

  1. periodic_slow_query_check (io.kestra.plugin.core.trigger.Schedule) runs every 10 minutes, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. check_historical_slow_queries (io.kestra.plugin.jdbc.clickhouse.Query, fetchType: FETCH_ONE) counts system.query_log rows with type = 'QueryFinish' in the lookback_minutes window, populating row.total_finished_queries, row.slow_query_count, row.max_duration_ms.
  3. check_active_long_queries (io.kestra.plugin.jdbc.clickhouse.Query, fetchType: FETCH_ONE) counts system.processes rows with elapsed > long_running_threshold_seconds, and picks the longest-elapsed one's query_id through a scalar subquery in the same statement, populating row.active_long_count, row.max_elapsed_seconds, row.oldest_active_query_id.
  4. evaluate_log_activity (io.kestra.plugin.core.flow.If) branches on total_finished_queries > 0.
    • else (UNREACHABLE): alert_query_log_unreachable reports that the query log is empty for the whole window.
    • then (log has data): nested evaluate_query_risk (io.kestra.plugin.core.flow.If) branches on slow_query_count > 0 or active_long_count > 0.
      • else (HEALTHY): log_query_health_healthy records a clean check.
      • then (AT_RISK): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", further gated by check_has_active_query - only a currently active query can be killed, not a finished one in the log - and then by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When every gate allows it, kill_long_running_query runs KILL QUERY WHERE query_id = 'oldest_active_query_id'. alert_query_risk always fires to Slack regardless of which remediation path ran.
  5. log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.
  6. The errors block alerts Slack separately if the flow itself fails outright - an unreachable cluster or bad credentials must not read as "no risk".

What you get

  • A three-state answer (HEALTHY / AT_RISK / UNREACHABLE) sourced from ClickHouse's own query execution history and live process list, not from an external APM agent.
  • Both halves of the slow-query problem covered in one gate: queries that already finished slowly (a trend signal) and queries still running too long right now (an actionable signal).
  • Remediation scoped to exactly one KILL QUERY on the single longest-running active query, by query_id - it never kills an arbitrary or unrelated query, and never touches a query that has already finished.
  • Two independent gates (auto_remediate, dry_run) between "risk detected" and "query actually killed".
  • A final status log every run, risk or not, so the execution history doubles as a query-performance trend.

Who it's for

  • Platform and database reliability teams running ClickHouse who need an early signal when queries start running long, before concurrency limits or memory pressure turn it into an outage.
  • On-call engineers who want one Slack alert with the exact slow-query and active-query numbers instead of running SELECT * FROM system.processes by hand during an incident.
  • Anyone evaluating the ClickHouse plugin who wants a worked example of Query driving two different system tables (system.query_log, system.processes) plus a KILL administrative statement, distinct from this repo's own system.mutations/system.parts watchdog and system.replicas health gate.

Why orchestrate this with Kestra

system.query_log and system.processes run by hand answer one moment in time; they do not decide a cadence, classify the result into a three-state risk level, pick a single deterministic remediation target, gate a kill behind two independent confirmations, or notify anyone. Kestra supplies the schedule, the HEALTHY/AT_RISK/UNREACHABLE classification as a first-class branch, two independent authorization gates before any query is killed, and an execution history that shows exactly when query performance degraded and whether anything was done about it.

Prerequisites

  • A reachable ClickHouse instance with query logging enabled (log_queries, on by default per ClickHouse's own documentation).
  • A database user with SELECT on system.query_log and system.processes, and, if auto_remediate will ever be "true", permission to run KILL QUERY.
  • A Slack incoming webhook for alerts.

Local testing: docker run -d --name clickhouse-slow-query-gate -p 8124:8123 -p 9001:9000 clickhouse/clickhouse-server:latest docker network connect YOUR_KESTRA_NETWORK clickhouse-slow-query-gate (check existing networks first with docker network ls; only needed if the Kestra Worker runs in a separate Docker network than this container; set clickhouse_url to jdbc:clickhouse://clickhouse-slow-query-gate:8123/default from inside that network, or jdbc:clickhouse://localhost:8124/default from the host). Host port 8124 is used here specifically so it never collides with Kestra's own UI on 8080.

Secrets

  • CLICKHOUSE_USERNAME / CLICKHOUSE_PASSWORD: credentials used by every io.kestra.plugin.jdbc.clickhouse.Query task in this flow.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_query_risk, alert_query_log_unreachable, and the errors block.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_CLICKHOUSE_PASSWORD=$(echo -n '<your-local-password>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • clickhouse_url (STRING, default jdbc:clickhouse://localhost:8124/default): JDBC URL.
  • lookback_minutes (INT, default 60): window scanned in system.query_log.
  • slow_query_threshold_ms (INT, default 5000): finished-query duration ceiling.
  • long_running_threshold_seconds (INT, default 60): active-query elapsed-time ceiling.
  • auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for remediation to even be considered.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for KILL QUERY to actually run.

Outputs

  • outputs.check_historical_slow_queries.row.total_finished_queries / .slow_query_count / .max_duration_ms: historical query metrics, every run.
  • outputs.check_active_long_queries.row.active_long_count / .max_elapsed_seconds / .oldest_active_query_id: live query metrics, every run; oldest_active_query_id is absent when active_long_count is 0.

Quick start

  1. Start ClickHouse locally with the command above and run a deliberately slow query (for example a large cross join or SELECT sleep(10) repeated) to populate both system.query_log and, while it runs, system.processes.
  2. Add the CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD, and SLACK_WEBHOOK_URL secrets.
  3. Run the flow manually with the defaults and confirm alert_query_risk fires once a query crosses either threshold.
  4. Run once with auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the kill without running it.
  5. Run once with auto_remediate: "true" and dry_run: "false" while a long query is still active, and confirm kill_long_running_query runs, then verify with SELECT * FROM system.processes that it is gone.
  6. Point lookback_minutes at a very short window on a quiet cluster and re-run to confirm alert_query_log_unreachable fires (UNREACHABLE) instead of either HEALTHY or AT_RISK.
  7. Enable periodic_slow_query_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.

How to extend

  • Add a breakdown by user or current_database from system.query_log to the Slack alert, so the team that owns the slow query is named, not just the count.
  • Add exception_code/exception from system.query_log (type IN ('ExceptionBeforeStart', 'ExceptionWhileProcessing')) as a fourth signal alongside slow and long-running queries.
  • Loop over several clickhouse_url values with io.kestra.plugin.core.flow.Loop to run the same gate across every node in a cluster, since system.processes is local to the node it is queried on.
  • Route the UNREACHABLE branch to a different Slack channel than AT_RISK, since an empty query log after a known-busy period often signals a logging or connectivity problem, not an idle cluster.

Pitfalls

  • UNREACHABLE here means "the query log has nothing for this window", not "the cluster is down". A fully unreachable ClickHouse instance fails the Query task's own connection and is caught by the errors block, not by evaluate_log_activity - that branch only fires when the query itself succeeds but returns zero QueryFinish rows.
  • A finished slow query cannot be killed. check_has_active_query exists specifically so an AT_RISK state caused only by historical entries in system.query_log is reported as unremediable, rather than silently doing nothing while claiming a kill was considered.
  • system.processes is per-node, not cluster-wide. On a multi-node cluster, this flow's check_active_long_queries only sees queries currently running on the node named in clickhouse_url - see How to extend for looping across nodes.
  • There is no structured "query log" or "kill query" task in this plugin. Verified from this repo's own existing ClickHouse flows (system.mutations, system.parts, system.replicas, system.users): all system-table introspection and all SYSTEM/KILL administrative statements go through the same generic io.kestra.plugin.jdbc.clickhouse.Query task.
  • auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for KILL QUERY to run; setting only one leaves the other still blocking.
  • Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.
  • This blueprint is UNTESTED against a live ClickHouse cluster. The system.query_log and system.processes column names are taken verbatim from ClickHouse's own system-tables documentation, and KILL QUERY syntax from ClickHouse's own SQL reference - but no Docker container or Kestra engine was run to execute this flow end to end.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.