Health Checks and Monitoring: Observing That the API Is Up and Healthy
We cover health checks and monitoring: automatically checking whether the application is healthy in production, observing dependencies like the database, the liveness/readiness distinction, and the basics of monitoring.
Your API now works correctly and fast. But after it goes live, a new question appears: is it actually up right now? Can it reach the database, or has it silently crashed? Waiting for someone to complain to find this out is a bad strategy. A good application reports its own health. Today we talk about health checks and monitoring: the ways to make your API not just working, but also observable.
Why Is a Health Check Needed?
While an application runs in production, many things can go wrong: the database connection drops, a service it depends on doesn't respond, the disk fills up. In these cases the application may seem to be "working" but is actually unable to do its job. The problem is that this is hard to understand from the outside; until a user gets an error and complains. And by the time you reach that point, it's too late.
Think of a health check like a patient monitor in a hospital. It continuously measures the patient's heartbeat, pulse, and oxygen, and instantly sounds an alarm when a value goes outside the normal range. No one waits for the patient to "say they feel bad"; the monitor catches the problem before they even notice it. A health check does this for your application too: it continuously asks "are you okay?" and reports early when there's a problem.
The Simplest Health Check
.NET supports health checks built-in. Setting it up in its simplest form is two lines. First you register the service:
builder.Services.AddHealthChecks();
Then you publish it as an endpoint:
app.MapHealthChecks("/health");
Now your application offers a simple health report at the /health address. When you make a request to this address, if everything's fine it returns a "Healthy" response and a 200 status code. If the application isn't up, this address can't be reached at all; if it's up but there's a problem, it returns an unhealthy response. The actual users of this endpoint aren't humans but automated systems; we'll come to that shortly.
Checking Dependencies
A simple health check only answers the "is the application up?" question. But the health of a real application also depends on the health of the things it's connected to. Your application may be running, but if it can't reach the database, it's actually unable to do its job. This is exactly why health checks are enriched to also check dependencies.
For example, you can add a health check that checks whether the database is reachable. There are ready-made packages for this in the .NET ecosystem; they offer ready-made checks for common dependencies like the database and Redis. The idea is: the health report now doesn't just say "is the application breathing?", it says "is the application really able to do work, can it reach everything it needs?" You can also include the Redis we added in the twenty-sixth article in this check and observe the "is my cache up?" question too.
Two Different Questions: Liveness and Readiness
There's an important distinction in health checks, and knowing it will help you a lot. We're actually asking two different questions, and these shouldn't be confused.
Liveness: "Is the application alive?" That is, is the process running, or has it completely crashed? If this check fails, the solution is usually restarting the application.
Readiness: "Is the application ready to take work?" The application may be up but may not have established the database connection yet or finished its startup operations. In this case it's alive but not in a state to take work.
Why does this distinction matter? Because their solutions are different. If an application is alive but not ready, restarting it doesn't solve the problem; you just need to wait for it to be ready. But if it has completely crashed, restarting is the right move. By doing these two checks separately, you enable automated systems to make the right decision. .NET lets you separate these two by tagging health checks.
Who Uses This Information?
You might be wondering who calls the /health endpoint. The answer: automated systems. It has two typical users. The first is the infrastructure that runs your application (for example, the container environments we'll touch on in the twenty-ninth article). These systems poll the /health address at regular intervals; if they get an unhealthy response, they can restart the application or stop sending traffic to that instance.
The second is monitoring tools. These continuously check the /health endpoint and, when they detect a problem, send an alert to the relevant people; an email, a message, a notification. So even while you're sleeping at night, when the database connection drops, your phone vibrates and you can intervene before users even notice.
Monitoring: More Than Health
A health check is the instant answer to the "are you okay?" question. Monitoring is a broader concept: continuously observing the application's behavior over time. You continuously collect the answers not just to "is it up?" but also to questions like "how fast is it responding, how many requests is it getting, what's the error rate, how's the memory usage?"
In the fourteenth article, while discussing logging, we laid the foundation of this culture; monitoring is a layer above that idea. While logs give you the answer to "what happened?", monitoring answers "how's the general trend, is there a pattern?" Together, the two provide full visibility into the application's life in production. This visibility is generally called "observability": the ability to understand from the outside what's going on inside the system.
A Small Experiment
First add the simplest health check to your project and go to the /health address to see the "Healthy" response. Then enrich it: add a health check that checks the database connection. Run the application and observe that /health now also reflects the database's state. Next, deliberately create a problem; for example, temporarily break the database connection information and see how /health returns "Unhealthy." This small experiment clearly shows that a health check isn't an abstract concept but a concrete tool that takes the application's pulse.
In the next article we prepare to deploy the application. The first step will be Docker: we'll talk about how to put the application into a portable package that will run the same way everywhere. The time is coming to bury the sentence "it worked on my machine." The health check we added today will come in handy exactly in that container environment.