If you have ever copied an n8n alerting rule from a tutorial, deployed it, and then waited weeks for an alert that never came, you are not doing anything wrong. The rule is. A large share of published n8n alert rules reference series that stock n8n does not emit under those names, so they sit in your config looking correct and evaluating to nothing forever.
An alert only fires when it is wired to a series that exists in your n8n version and moves when the thing you care about breaks. Almost every dead n8n alert violates one of those two conditions: it uses an invented series name, or it targets a metric that is absent in the version you run. The fix is not a different monitoring stack. It is matching each rule to a series your instance emits, then proving it transitions to firing.
This guide works through that, section by section, removing one reason an alert fails in the order those reasons bite: wrong mental model, wrong series name, missing scrape target, missing dependency, an expression that silently never evaluates, an undelivered notification, and finally an untested rule.
Prerequisites and scope
You need a self-hosted n8n instance, single-node or queue mode, with Prometheus and Alertmanager reachable, and basic PromQL familiarity. Two things to state up front:
The /metrics endpoint is self-hosted only. It is not available on n8n Cloud. The official monitoring documentation covers this.
Metric availability is version-dependent. This guide targets n8n 2.x, with the current release being 2.23.2 (n8n 2.0 shipped in December 2025). Because n8n adds and renames metrics between minor versions, the single most important habit this guide will push is to check your own /metrics output rather than trust any list, including this one.
The two questions your monitoring has to answer
Before writing a single rule, separate these two questions. They use different series and different reasoning, even though both are answerable from /metrics in current 2.x.
Is n8n healthy? This is about the runtime: is the process up, is the event loop responsive, is memory climbing toward the container limit, is the queue draining. The data lives in the default Node.js process metrics, the queue gauges, and the standard exporters.
Did my workflow succeed? This is about correctness: did executions error, what is the failure rate. In current n8n 2.x this is answerable natively, because the execution-duration histogram carries a success or failure status label and is on by default. The reason copied failure rules still fail is not that the data is missing; it is that the rules use series names n8n never emitted. We confront the exact names later.
Keep the two questions in different mental buckets and the rest follows. The runtime question gets process and queue rules. The correctness question gets the execution histogram, with the Error Trigger and a Postgres query as complements for immediacy and per-workflow breakdown.
What the /metrics endpoint exposes, and what it does not
n8n collects metrics through the prom-client library, and the honest inventory is thinner than most guides imply. The bulk of /metrics is prom-client default Node.js process metrics. The n8n-specific series are few and mostly opt-in.
Metrics are grouped into categories, each behind an environment variable. The defaults matter, because four of the five interesting categories are off by default:
| Category | Environment variable | Default |
|---|---|---|
| Node.js process metrics | N8N_METRICS_INCLUDE_DEFAULT_METRICS | true |
| Cache hit/miss | N8N_METRICS_INCLUDE_CACHE_METRICS | false |
| Event bus | N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS | false |
| API endpoints | N8N_METRICS_INCLUDE_API_ENDPOINTS | false |
| Queue (queue mode) | N8N_METRICS_INCLUDE_QUEUE_METRICS | false |
There are also label toggles, all off by default because they increase cardinality: N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL, N8N_METRICS_INCLUDE_WORKFLOW_NAME_LABEL, N8N_METRICS_INCLUDE_NODE_TYPE_LABEL, and N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL. The n8n-specific metric prefix defaults to n8n_ and is configurable via N8N_METRICS_PREFIX. These are documented on the endpoints environment variables page, and the list grows between releases, so confirm it against your version.
Two metric toggles sit outside that category table and matter for the failure question later. N8N_METRICS_INCLUDE_WORKFLOW_EXECUTION_DURATION defaults to true, so the execution-duration histogram is emitted as soon as metrics are on. N8N_METRICS_INCLUDE_WORKFLOW_STATISTICS defaults to false, and it gates a separate set of cached total-count gauges (n8n_production_executions, n8n_manual_executions, and similar) that report execution totals but are not split by success or failure.
The series you can build runtime alerts on, from the default Node.js set, are stable and standard:
process_resident_memory_bytesfor resident memoryprocess_cpu_user_seconds_totalandprocess_cpu_system_seconds_totalfor CPUnodejs_eventloop_lag_secondsfor event loop responsivenessprocess_open_fdsandprocess_max_fdsfor file descriptor exhaustionnodejs_gc_duration_secondsfor garbage collection pressure
The n8n-specific series are fewer, but two of them carry most of the value. In queue mode, the queue depth gauge is n8n_scaling_mode_queue_jobs_waiting. And the workflow execution-duration histogram, n8n_workflow_execution_duration_seconds, is emitted by default and labelled by status (success or failed) and mode (manual, trigger, webhook, and so on); its _count child series is the native source of execution and failure counts. Both names matter, because both are names that widely copied rules get wrong.
A note on version drift, since it is the reason “check your own endpoint” is not optional. The execution-duration histogram was introduced during the 2.x line, so a very old 2.x build or a 1.x instance may not have it. Separately, an active-workflow-count gauge appears in the source but was reported missing from the metrics output in a 1.x build. Confirm what your specific version emits rather than assuming.
Verify before you alert. Curl your own endpoint and read it:
curl -s http://127.0.0.1:5678/metrics | grep -E "n8n_|nodejs_eventloop|process_resident". The exact set of n8n-specific series, including the execution-duration histogram and its labels, varies by version. Treat the lists in any guide as a starting point to confirm, not as ground truth for your instance.
Enabling and scraping it correctly
Turning metrics on is one variable. Scraping them correctly in queue mode is where data quietly goes missing.
Enable the endpoint and the categories you want. In a Compose setup for queue mode, the n8n service environment looks like this. This targets n8n 2.23.2, Postgres 18, and Redis 8; pin to whatever versions you run.
services: n8n-main: image: docker.n8n.io/n8nio/n8n:2.23.2 environment: - N8N_METRICS=true - N8N_METRICS_INCLUDE_DEFAULT_METRICS=true - N8N_METRICS_INCLUDE_QUEUE_METRICS=true - QUEUE_HEALTH_CHECK_ACTIVE=true - EXECUTIONS_MODE=queue - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - QUEUE_BULL_REDIS_HOST=redis depends_on: - postgres - redis
n8n-worker: image: docker.n8n.io/n8nio/n8n:2.23.2 command: worker environment: - N8N_METRICS=true - N8N_METRICS_INCLUDE_DEFAULT_METRICS=true - QUEUE_HEALTH_CHECK_ACTIVE=true - EXECUTIONS_MODE=queue - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - QUEUE_BULL_REDIS_HOST=redis depends_on: - postgres - redis
postgres: image: postgres:18 environment: - POSTGRES_USER=n8n - POSTGRES_PASSWORD=REPLACE_WITH_STRONG_PASSWORD - POSTGRES_DB=n8n volumes: - postgres-data:/var/lib/postgresql/data
redis: image: redis:8-alpine volumes: - redis-data:/data
volumes: postgres-data: redis-data:Two queue-mode details cause most of the lost data.
First, every process exposes its own endpoint. The main, worker, and webhook processes each serve /metrics separately, and you must scrape every one of them. The common failure is setting N8N_METRICS=true on main only, or pointing Prometheus at the main instance alone, and then wondering why worker memory or CPU never shows up. Queue metrics specifically (n8n_scaling_mode_queue_jobs_waiting and the related _active, _completed, and _failed series) are emitted by the main process, but the Node.js process metrics you care about for capacity live on each worker. One multi-main caveat: every main emits its own copy of these queue gauges for the same shared queue, so summing them across mains multiplies the real figure. The Prometheus configuration docs point you at the instance_role_leader gauge to single out the leader’s view when you aggregate.
A scrape config for queue mode targets each role and labels it so you can aggregate later:
scrape_configs: - job_name: "n8n-main" metrics_path: /metrics static_configs: - targets: ["n8n-main:5678"] labels: role: "main" - job_name: "n8n-workers" metrics_path: /metrics static_configs: - targets: ["n8n-worker-1:5678", "n8n-worker-2:5678"] labels: role: "worker"When you graph or alert on worker series, aggregate across instances rather than reading one worker, for example sum without (instance) (process_resident_memory_bytes{role="worker"}).
Second, worker health endpoints need an explicit toggle. The liveness path /healthz and the readiness path /healthz/readiness are only active on workers when QUEUE_HEALTH_CHECK_ACTIVE=true is set, as documented in the queue mode docs. Without it, any readiness-based probe on a worker has nothing to talk to and fails silently in the unhelpful direction.
One security note: the /metrics endpoint is unauthenticated and exposes operational detail. Keep Prometheus on a private network and restrict the endpoint at the network or reverse-proxy layer. Do not expose it to the internet.
The dependencies n8n will not report on
A large share of real n8n incidents start in Postgres or Redis, and n8n’s own /metrics says nothing about either. If the database connection pool saturates or Redis becomes unreachable, n8n’s process metrics may look fine right up until executions stall. That is a hole exactly where outages begin.
Close it with the standard exporters, scraped alongside n8n:
- postgres_exporter (current release 0.19.1) for
pg_up, connection counts viapg_stat_activity_count, and replication or transaction-age signals - redis_exporter for Redis reachability and memory
- node_exporter (current release 1.11.1) for host CPU, memory, and disk, plus cAdvisor for per-container memory against the container limit
Keep this layer focused. You are not building a generic exporter tutorial; you want the few dependency signals that change n8n’s behavior when they go bad: database reachable, connection pool not saturated, Redis reachable, host and container memory not exhausted. Install each exporter per its own documentation and move on.
Alerts that actually fire on runtime health
Here are copy-ready rules for the signals /metrics and the exporters support. Each has a for: duration so a transient blip does not page you, and each is wired to a series that exists and moves when the thing you care about breaks.
Before the rules, the trap that defeats more alerts than any other. In Prometheus, a counter that has never incremented produces no time series at all. An expression like rate(some_total[5m]) > 0 against a series that does not exist does not evaluate to false; it evaluates to nothing, and an alert on nothing never fires. This is the literal mechanism behind “alerts that do not fire”. Defend against it with up == 0 and absent(), which fire precisely when data is missing.
groups: - name: n8n-runtime rules: - alert: N8nInstanceDown expr: up{job=~"n8n-.*"} == 0 for: 2m labels: severity: critical annotations: summary: "n8n process {{ $labels.instance }} is not being scraped"
- alert: N8nTargetMissing expr: absent(up{job="n8n-main"}) for: 5m labels: severity: critical annotations: summary: "n8n main target has disappeared from Prometheus entirely"
- alert: N8nEventLoopLagHigh expr: nodejs_eventloop_lag_seconds{job=~"n8n-.*"} > 0.2 for: 10m labels: severity: warning annotations: summary: "n8n event loop lag is sustained above 200ms on {{ $labels.instance }}"
- alert: N8nContainerMemoryHigh expr: container_memory_working_set_bytes{name=~"n8n.*"} / container_spec_memory_limit_bytes{name=~"n8n.*"} > 0.9 for: 10m labels: severity: warning annotations: summary: "n8n container {{ $labels.name }} is above 90% of its memory limit"
- alert: N8nQueueBacklogGrowing expr: n8n_scaling_mode_queue_jobs_waiting > 50 for: 10m labels: severity: warning annotations: summary: "n8n queue backlog has stayed above 50 waiting jobs for 10 minutes"
- alert: PostgresDown expr: pg_up == 0 for: 1m labels: severity: critical annotations: summary: "Postgres backing n8n is unreachable"A few notes on why these are written this way.
N8nInstanceDown uses up == 0, which is true whenever Prometheus has a target it cannot scrape. N8nTargetMissing uses absent() to catch the different failure where the target vanishes from service discovery and up produces no series at all. The two cover complementary gaps.
The memory rule uses cAdvisor’s container working set against the container’s own memory limit, rather than joining n8n’s process_resident_memory_bytes to a limit series. That join is fiddly because the process metric and the cAdvisor metric carry different labels and do not line up on a common key without rewriting. Letting cAdvisor compute the ratio against the limit it already knows is cleaner and correct. One caveat: this assumes the container has a memory limit set, since without one container_spec_memory_limit_bytes reports the host’s total memory and the ratio becomes meaningless. Set a limit on the n8n containers, and keep process_resident_memory_bytes for trend and leak detection on a dashboard.
The queue backlog threshold of 50 is a placeholder. The useful signal is backlog rising while worker CPU stays flat, which means you are under-provisioned on workers or blocked downstream on Postgres or an external API. Tune the number to your throughput.
Readiness deserves a clarification the candidate list often glosses over. /healthz/readiness returns 200 only when the database is connected and migrations are done, which is useful, but it is an HTTP endpoint, not a Prometheus series. To alert on it you scrape it with blackbox_exporter and alert on probe_success == 0 for that target. If you do not run blackbox_exporter, up == 0 on the metrics scrape still covers the case where the process is gone; it just will not distinguish “running but database disconnected” from “down”.
The alert everyone actually wants: my workflow failed
This is the section that justifies the whole article, and it is where the precision has to be sharpest, because the common advice here is both confidently stated and wrong for current n8n.
The wrong version of the story is that n8n emits no workflow-failure metric, so you have to leave Prometheus and query the database. That was once a reasonable read, but it is not true for 2.x. The reason widely copied failure rules do not fire is simpler and more specific: they use series names n8n never emitted. A rule like rate(n8n_execution_failed_total[5m]) > 0.1 never fires because n8n_execution_failed_total is not a real series. The same rule sets often pair it with n8n_queue_bull_queue_waiting for queue depth, where the series n8n emits is n8n_scaling_mode_queue_jobs_waiting. Wrong name, no series, no evaluation, forever. Check any inherited rule against your own /metrics dump before trusting it.
What does exist, by default, is the execution-duration histogram n8n_workflow_execution_duration_seconds. It carries a status label of success or failed and a mode label, and because N8N_METRICS_INCLUDE_WORKFLOW_EXECUTION_DURATION defaults to true, you already have it the moment N8N_METRICS=true is set. Its _count child series is a native, status-aware execution counter, which is exactly the thing the failure alert needs. The failure rate is a direct Prometheus query:
rate(n8n_workflow_execution_duration_seconds_count{status="failed"}[5m])For a proportion rather than an absolute rate, divide failed by total:
sum(rate(n8n_workflow_execution_duration_seconds_count{status="failed"}[5m])) /sum(rate(n8n_workflow_execution_duration_seconds_count[5m]))That turns into a rule like this. The mode label lets you scope to production runs and ignore manual editor executions:
- alert: N8nWorkflowFailureRateHigh expr: | sum(rate(n8n_workflow_execution_duration_seconds_count{status="failed", mode!="manual"}[5m])) / sum(rate(n8n_workflow_execution_duration_seconds_count{mode!="manual"}[5m])) > 0.05 for: 10m labels: severity: warning annotations: summary: "More than 5% of production workflow executions are failing"One caveat that keeps this honest: by default the histogram is not labelled per workflow, so this rule tells you the instance-wide failure rate, not which workflow is failing. You can add the workflow_id label with N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL, but that multiplies cardinality by the number of active workflows, which is exactly the kind of explosion you do not want in Prometheus on a busy instance. So the native metric answers “are failures spiking” well, and “which workflow” poorly. That is where the two complements come in.
The n8n Error Trigger, for immediacy and identity. n8n’s built-in Error Trigger node does not fire on every error globally. It runs only when its workflow is designated as the error workflow for another workflow (or for itself), and only for automatic executions, not manual editor runs. Set one error-handler workflow as the error workflow for the workflows you care about, and it fires the instant one of them fails, with the failed workflow’s name and the error in the payload. Route that to Slack or email, or post it into Alertmanager’s /api/v2/alerts endpoint so it flows through the same routing, grouping, and silencing as your infrastructure alerts. This gives you the per-incident detail the histogram omits.
A Postgres query, for per-workflow breakdown without cardinality cost. n8n stores execution records, including status and workflow, in its database. A read-only role and a scheduled query give you per-workflow failure rates over arbitrary windows without adding a single high-cardinality label to Prometheus. This is the right tool for “what is the failure rate of this specific workflow over the last day”, and it is the basis of the analytics-style dashboards people build for n8n. Keep it on a tightly scoped read-only role so monitoring never contends with production writes.
Insights rounds this out in-app: a summary view is available across plans, while the deeper per-workflow failure-rate breakdowns are a paid feature. It is convenient for eyeballing trends, but it sits outside your alerting pipeline rather than inside it.
The practical recommendation: alert on the native status="failed" rate in Prometheus, use the Error Trigger for instant per-workflow notifications, and reach for the Postgres query when you want per-workflow rates without paying the cardinality tax. The native metric is the foundation here, not an afterthought.
Wiring Alertmanager so notifications arrive
A firing rule is not a delivered notification. Alertmanager decides what reaches a human and what gets suppressed.
route: receiver: "default" group_by: ["alertname", "role"] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - matchers: - severity = "critical" receiver: "oncall" repeat_interval: 1h
receivers: - name: "default" slack_configs: - api_url: "REPLACE_WITH_SLACK_WEBHOOK_URL" channel: "#n8n-alerts" send_resolved: true - name: "oncall" slack_configs: - api_url: "REPLACE_WITH_SLACK_WEBHOOK_URL" channel: "#oncall" send_resolved: true
inhibit_rules: - source_matchers: - alertname = "N8nInstanceDown" target_matchers: - severity = "warning" equal: ["instance"]The pieces that matter:
group_by collapses related alerts into one notification so a single outage does not produce a wall of messages. repeat_interval controls how often an unresolved alert re-notifies; tighter for critical, looser for warnings. The routes block sends critical alerts to a separate, faster receiver.
The inhibit_rules block is the one people skip and then regret. When n8n is down, every downstream warning, high memory, queue backlog, readiness failure, becomes noise about a symptom you already know. Inhibiting warnings while the instance-down alert is active keeps the page readable. Extend the same idea to suppress workflow-failure alerts when the whole instance is down, since “the workflow failed” is not actionable when the cause is “n8n is not running”.
Point Prometheus at Alertmanager in prometheus.yml:
alerting: alertmanagers: - static_configs: - targets: ["alertmanager:9093"]This guide assumes Prometheus 3.x and Alertmanager 0.32.x, the current stable lines.
Proving the alert fires
This is the step nearly every tutorial omits, and it is the one that separates an alerting setup you trust from a folder of YAML you hope works.
For each rule, induce the failure on purpose and watch the alert move through its states. In the Prometheus UI under Alerts, a rule with a for: duration goes from inactive to pending the moment its expression is true, then to firing once the for: window elapses. Then confirm the notification lands in Slack or your inbox.
Concrete tests:
- Stop a worker container and confirm
N8nInstanceDownreaches firing and pages. - Stop Postgres, or block its port, and confirm
PostgresDownfires. Simulating a database disconnect is the highest-value single test, because the database is where many real n8n incidents originate. - Force a workflow error, for example with a deliberately failing HTTP Request node, and confirm your Error Trigger handler sends its notification.
- Load the workers until
n8n_scaling_mode_queue_jobs_waitingclimbs past your threshold and confirm the backlog alert fires.
An alert you have never seen fire is an assumption, not a safeguard. Test each one once, on purpose, before you rely on it.
Decision matrix
When you want a signal, the question is always the same: which source carries it, and can Prometheus alert on it directly.
| Signal you want | Right source | Example metric or mechanism | Alertable directly in Prometheus? |
|---|---|---|---|
| n8n process up | /metrics scrape target | up == 0, absent() | Yes |
| Event loop or runtime stress | /metrics default set | nodejs_eventloop_lag_seconds, process_resident_memory_bytes | Yes |
| Container memory vs limit | cAdvisor | container_memory_working_set_bytes / container_spec_memory_limit_bytes | Yes |
| Queue backlog (queue mode) | /metrics queue metrics | n8n_scaling_mode_queue_jobs_waiting | Yes |
| Database or Redis down | exporters | pg_up, redis_exporter series | Yes |
| Readiness (database connected) | blackbox_exporter probing /healthz/readiness | probe_success == 0 | Yes, with blackbox_exporter |
| Workflow failure rate | /metrics execution histogram | rate(n8n_workflow_execution_duration_seconds_count{status="failed"}[5m]) | Yes, native and on by default in 2.x |
| Which workflow failed | Error Trigger; Postgres executions data | per-incident notification; scheduled per-workflow query | Complement to the native rate (avoids label cardinality) |
The pattern across the table: nearly everything you want is alertable directly in Prometheus, including workflow failure rate, as long as the rule names a series your version emits. The instance-wide failure rate comes from the native histogram; the Error Trigger and a Postgres query fill in which workflow failed without forcing a high-cardinality label into your metrics. Wire each rule to a series that exists and moves when something breaks, prove it fires, and the alerts hold up.