
From Health Check to Real Functional Test: How We Monitor n8n and Qdrant
We've been using n8n in production for quite some time, both internally and for customer projects. Earlier on this blog, Christian explained how we built an AI powered spam protection workflow with n8n. Regular readers may also know that Gatus is our monitoring solution of choice. One thing was still missing, though. While we monitored whether our infrastructure was reachable, we had no reliable way of verifying that n8n and our Qdrant vector database were actually working as expected. Since both are essential building blocks for many of our AI based workflows, we wanted something more meaningful than a simple uptime check.
What started as "let's add a health check" quickly turned into a deeper look at what production monitoring should really verify.
Step 1: Ready?
Getting Health Checks Right
Most modern applications expose one or more health check endpoints. They are designed for monitoring systems such as Gatus, which periodically request a URL and raise an alert if the expected response changes.
There's a distinction worth making here, one that originated in the Kubernetes world and has since spread well beyond it: liveness versus readiness. A liveness check answers a very simple question: Is the process still running? Nothing more. A readiness check asks something more useful: Is the application currently able to serve requests? A process can be running and still not be ready, for instance because its database connection just dropped.
n8n exposes dedicated endpoints for both checks. /healthz is a liveness endpoint. A HTTP 200 response simply confirms that the process is alive. /healthz/readiness performs additional checks. It only returns HTTP 200 once the database connection has been established and all database migrations have completed. Only then is n8n actually ready to execute workflows. If you're running n8n in production without digging into what's underneath it, the readiness endpoint already gives you a far more meaningful check than its plain name suggests.
That's why we test both endpoints separately, one for /healthz and one for /healthz/readiness.
endpoints:
- name: n8n-health
url: "https://n8n.example.com/healthz"
interval: 5m
conditions:
- "[STATUS] == 200"
- name: n8n-readiness
url: "https://n8n.example.com/healthz/readiness"
interval: 5m
conditions:
- "[STATUS] == 200"That's a solid, sensible first step, and it's where most monitoring setups stop. Fair enough, that's exactly what a readiness endpoint is built for.
Step 2: Actually Done?
When "Ready" Doesn't Mean "Working"
For us, step one isn't the finish line, it's the foundation. /healthz/readiness tells nothing about whether the workflow built on top of it is doing what it's supposed to do. A workflow may be fully reachable while still failing halfway through its execution. A node might throw an exception, an AI classification could produce an unexpected result, or an external service might stop responding.
From an infrastructure perspective, everything still looks healthy. From a business perspective, the automation has already failed. In complex automation scenarios involving forms, AI models, vector databases, APIs and email delivery, these are two fundamentally different questions.
What ultimately matters is whether the workflow completed successfully.
Fortunately, n8n exposes its execution history through the REST API. The API returns execution metadata as JSON, allowing us to inspect the most recent workflow runs.
The REST API lets you filter by status and number of executions, but not by a time period. Gatus, on the other hand, doesn't provide date based functions. That creates an interesting challenge. If only the latest execution is checked, a successful run could easily hide an earlier failure that occurred just a few minutes before. Instead, we combine several conditions that together provide a much more reliable picture.
Here's what that looks like in the Gatus config.
endpoints:
- name: n8n-workflow-spam-protection
url: "https://n8n.example.com/api/v1/executions?workflowId=123&limit=5"
headers:
X-N8N-API-KEY: "${N8N_API_KEY}"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[CERTIFICATE_EXPIRATION] > 120h"
- "[BODY].data[0].status == success"
- '[BODY] != pat(*"status":"error"*)'Four conditions that together say more than any single one on its own:
[STATUS] == 200: the n8n API is responding at all.[CERTIFICATE_EXPIRATION] > 120h: The TLS certificate is still valid for at least another five days. Certificate expirations are easy to overlook and often cause unnecessary outages.[BODY].data[0].status == success: the most recent run of the workflow succeeded.[BODY] != pat(*"status":"error"*): none of the last five runs (fetched vialimit=5) contain a"status":"error"anywhere in the response.
The last two conditions look redundant, but they're deliberately not. The first is precise, it looks at exactly the most recent run. The second is broader, it just checks that none of the recent runs contain an error. Besides requiring less configuration than checking every execution individually, it also works regardless of how many execution records are actually returned.
One important difference compared to the health endpoints is that the REST API requires authentication.
Where we draw the line
One question remains intentionally unanswered.
Not every failed execution represents an operational problem. If a workflow gets tested manually during development and fails, that's not an operational problem, but it shows up in the execution history exactly like a real production failure would. n8n does record how each execution was triggered, so separating "tested in the editor" from "ran in production" would be possible in principle. Whether that additional complexity is worth implementing remains to be seen. For now, we're deliberately keeping the monitoring solution simple and will only extend it if practical experience shows that the distinction provides real operational value.
Step 3: A Different Call
Why Databases Need a Different Kind of Check
For n8n, going beyond the readiness check mattered for how the whole system runs. Databases are different. Checking whether the data itself is "correct" quickly becomes application specific and is therefore no longer a health check. For Qdrant we therefore rely on the built-in health endpoints, /healthz and /readyz. The same distinction applies: /healthz only confirms the process is responding, /readyz only returns a HTTP-Code 200 once the collections are actually loaded.
endpoints:
- name: qdrant-health
url: "https://qdrant.example.com/healthz"
interval: 5m
conditions:
- "[STATUS] == 200"
- name: qdrant-readiness
url: "https://qdrant.example.com/readyz"
interval: 5m
conditions:
- "[STATUS] == 200"A Practical Observation
Automation that gets built once and then left to run on its own carries essentially the same risk as a server without backups, except nobody notices until the outage has already cost something. If your production environment depends on n8n, AI based workflows or vector databases, these systems deserve the same level of operational monitoring as any other critical infrastructure.
What's Next
For now, we continue observing how the current setup performs in everyday operation. Rather than adding further layers upfront, we prefer to evolve the solution based on real world experience. If distinguishing between production failures and development related executions eventually proves valuable, that will be the next logical step. But only when the need actually arises.
Please feel free to share this article.
Comments
No comments yet.