Guide · for a developer or lead
Knowing Your .NET App Is Healthy After a Deploy
Will Pickeral, William Belle LLC · support@williambelle.co
A pipeline tells you the deploy succeeded. It does not tell you the application works. A release that publishes cleanly and then throws on every request is an ordinary outcome, and without something watching, the thing that tells you is a customer email an hour later.
This is what to put in place on Azure so that does not happen, and — the harder half — what to alert on so the alerts still mean something in six months.
Azure's documentation covers how to configure each piece. This is about which pieces, and the four decisions that separate useful alerting from an inbox everyone filters away.
Three questions, not one
"Is it healthy" is really three questions with different answers and different instruments.
Is it up? Something outside your infrastructure asks for a page and gets one. This catches the failures that take everything else down with them — a bad DNS change, an expired certificate, a network rule that locked out the world — and it is the only check that still works when the application cannot report on itself.
Is it working? Requests are succeeding, dependencies are answering, and the error rate is what it was yesterday. This is what a deploy breaks, and it is the question a pipeline cannot answer for you.
Is it about to stop? A disk filling, a connection pool at its limit, a certificate expiring in a week, a database approaching its size ceiling. These are the ones you want to hear about during working hours, and they are the ones teams skip because nothing is broken yet.
Instrument all three or you will keep being surprised by whichever one you left out.
Application Insights, and the sampling trap
For a .NET application on Azure the answer to "is it working" is Application Insights. Add the SDK, set one application setting, and requests, dependencies, and unhandled exceptions arrive without further code:
APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...;IngestionEndpoint=...
Use a workspace-based resource, which stores telemetry in a Log Analytics workspace and is what the current tooling assumes.
Then find the sampling setting before you trust a single number. The ASP.NET Core SDK turns on adaptive sampling by default. Under load it keeps a fraction of telemetry and records how many items each kept one represents. Azure's own charts multiply that back out, so aggregate counts are roughly right.
What is not roughly right is your search. A specific failed request may simply not be there, and a query you write yourself over requests returns the sampled rows, not the estimate. The two ways this bites: somebody concludes an error stopped happening when it was only sampled away, and somebody writes a KQL alert whose threshold silently means something different under load than it did in testing.
Decide deliberately. Keep sampling and use itemCount in your queries, or turn it off for a low-traffic application and pay for full fidelity. Either is defensible. Not knowing which one you are on is not.
Alert on what a customer would notice
The instinct is to alert on the causes: CPU, memory, thread count. It produces an inbox nobody reads, because a machine at 90% CPU serving every request correctly is not a problem, and it fires at 3am anyway.
Alert on symptoms. The short list, and it is genuinely short:
- An availability test failing from more than one region. One region failing is usually the test's network, not yours. Requiring two removes most false alarms without hiding a real outage.
- Server-side failure rate above its normal level. Not "any 5xx" — every application has a background rate. Alert on a multiple of what a normal hour looks like.
- A dependency failing. The database, the payment provider, the mail service. This is the single most useful alert after availability, because it tells you the problem is not your code and saves the first twenty minutes of every incident.
- Response time at the 95th percentile crossing a number you chose. The average hides the failure — it stays comfortable while a tenth of your users wait ten seconds.
- Certificate and domain expiry. Unglamorous, and it takes sites down every week. Thirty days is the right warning, not seven.
That is the set that pays for itself. Everything else — resource-level metrics, queue depths, cache hit rates — belongs on a dashboard somebody looks at deliberately, not in an alert that wakes a person.
Two rules keep the list honest. Every alert names what the person receiving it should do; an alert with no action is a notification, and notifications belong in a daily summary. And an alert that fires repeatedly without anyone acting gets deleted or its threshold fixed, never muted — a muted alert is a lie the team tells itself about its own coverage.
Make sure it reaches a person
An alert rule with no action group is a row in a table nobody visits. Azure will let you create one and it will fire into nothing.
Send to a place that is already part of the working day: a shared inbox, a chat channel, or an on-call schedule if the application genuinely warrants one. Then test it by causing a real alert — stop the application in a non-production environment and confirm someone actually receives it. The first real incident is a poor time to find out the action group had a typo in the address.
Whoever receives an alert must be able to act on it. An alert routed to a mailbox one person reads on weekdays is a working-hours alert, whatever the severity says, and describing it as anything else is how a team convinces itself it has cover it does not have.
Watch the window after a release
Most failures you will ever have arrive within minutes of a deploy, which is the one moment you know something changed.
Send a deployment annotation from the pipeline so the release appears on your Application Insights charts. A spike in exceptions is a different conversation when a marker sits directly underneath it.
Then watch for a defined period before calling the release done. Fifteen minutes covers most of it. What you are looking for is the failure rate and dependency failures returning to what they were before, not to zero — zero is not the normal state of a real application, and treating it as one produces a rollback for every unhandled edge case that was always there.
If you deploy through a slot swap, this window is where the value of the slot is realized: the previous version is still sitting there, and reversing is a swap rather than a rebuild.
Logs you can search
Telemetry tells you the rate. Logs tell you what happened to one customer at 14:32, which is the question you will actually be asked.
Two things make that possible:
Structured logging. logger.LogInformation("Order {OrderId} failed for {CustomerId}", orderId, customerId) records the values as fields you can query. The interpolated version, $"Order {orderId} failed", records a string you can only match on with a wildcard. Same effort, and one of them is searchable a year later.
Correlation. Application Insights sets an operation ID across the request and its dependencies, so a failed request and the SQL call underneath it are one trace rather than two coincidences. Log inside that scope and your own lines join the same trace. This is what turns "the site was slow" into "the site was slow because that one query was."
Keep the retention you would actually use. Ninety days answers nearly every question anyone asks, and telemetry is billed by volume ingested, so the difference between thoughtful and thoughtless instrumentation is a real number on a real invoice.
The daily cap will blind you
Application Insights has a daily ingestion cap, and teams set one after their first surprising bill.
When the cap is hit, ingestion stops until the next day. Not sampled harder — stopped. Your application keeps running and you stop being able to see it, and the day you hit the cap is disproportionately likely to be a day something is wrong, because something wrong is what produced the volume.
If you set one, set an alert on approaching it, and treat hitting it as an incident rather than a billing event. The better fix is usually to reduce what you send: debug-level logs from a chatty library, health-check requests recorded as real traffic, and a dependency call inside a loop are the three that account for most surprising volume.
If you have none of this today
In order. Each step is worth having on its own.
- An availability test on your production URL, alerting from two regions to an address a person reads. Ten minutes of work, and it catches the failures that take everything else down with them.
- Application Insights connected, with the sampling setting checked and written down.
- The failure-rate and dependency alerts. These are the two that tell you a deploy went wrong before a customer does.
- Deployment annotations from the pipeline, so a chart can answer "did this start with the release."
- Structured logging with correlation, which is a code change and the slowest of these, but the one that shortens every future incident.
Steps 1 through 3 take an afternoon and remove most of the case for finding out from a customer.
What this does not solve
Watching an application does not fix it, and a dashboard nobody opens on a quiet week is not coverage. The value of all of this shows up on the day something breaks, which is exactly why it never feels urgent while it is missing.
It also says nothing about whether the thing you deployed was correct. A release with a logic error runs cleanly, alerts nothing, and quietly produces wrong numbers — and no amount of instrumentation substitutes for someone reading what changed before it went out.
Get a free 20-minute review — tell me how you find out when something breaks today and I'll tell you where I'd start, and why. Nothing to prepare.