Zero-downtime deployments for C# ASP.NET apps are surprisingly achievable by treating your application instances as ephemeral cattle, not cherished pets.
Let’s watch this in action. Imagine we have a simple ASP.NET Core app, MyWebApp, running on two IIS servers behind a load balancer.
GET /
Host: mywebapp.example.com
HTTP/1.1 200 OK
Content-Type: text/plain
Server: Kestrel
Hello from Instance 1!
Now, we want to deploy a new version. The core idea is to introduce the new version alongside the old one, gradually shifting traffic.
The Setup
- Load Balancer: This is critical. It could be an Azure Load Balancer, AWS ELB, Nginx, HAProxy, or even IIS ARR. Its job is to distribute incoming requests across healthy application instances. It must support health checks.
- Multiple Application Instances: You need at least two instances of your application running. These could be separate IIS Application Pools, separate Docker containers, or VMs.
- Deployment Mechanism: A way to deploy the new version to new instances without touching the old ones until traffic is shifted. This is often automated via CI/CD pipelines (Jenkins, Azure DevOps, GitHub Actions).
The Zero-Downtime Deployment Process (The "Blue/Green" or "Canary" Pattern)
Let’s say our current version is v1 and we’re deploying v2.
-
Deploy
v2to New Instances:- Provision new infrastructure (e.g., new VMs, new containers).
- Deploy
v2of your ASP.NET application to these new instances. - Crucially, do not point your load balancer to these new instances yet. They are running, but invisible to end-users.
-
Health Check New Instances:
- Configure your load balancer’s health check to point to a specific endpoint in your application (e.g.,
/health). - Your application should implement this
/healthendpoint. Forv2, it should return200 OKonly when it’s fully ready, all dependencies are available, and it’s ready to serve traffic. - The load balancer will continuously poll
/healthon thev2instances. Once they pass the health check, they are considered "ready."
- Configure your load balancer’s health check to point to a specific endpoint in your application (e.g.,
-
Shift Traffic Gradually (Canary Deployment):
- Configure the load balancer to send a small percentage of new traffic (e.g., 5%) to the
v2instances. The remaining 95% still goes tov1. - Monitor application metrics (error rates, latency, resource utilization) for both
v1andv2instances. Ifv2shows any issues, you can immediately revert by removing it from the load balancer’s pool.
- Configure the load balancer to send a small percentage of new traffic (e.g., 5%) to the
-
Shift More Traffic:
- If
v2is stable, gradually increase the traffic percentage to it (e.g., 25%, 50%, 75%) while decreasing traffic tov1. - Continue monitoring.
- If
-
Full Cutover:
- Once 100% of traffic is successfully routed to
v2and it’s performing well, thev1instances are no longer receiving user requests.
- Once 100% of traffic is successfully routed to
-
Decommission Old Instances:
- Now you can safely shut down or remove the old
v1instances.
- Now you can safely shut down or remove the old
Example /health Endpoint Implementation (ASP.NET Core):
In Program.cs (or Startup.cs for older .NET Core versions):
// For ASP.NET Core 6+ Minimal APIs
app.MapGet("/health", () => Results.Ok("Healthy"));
// For older ASP.NET Core versions
// In Startup.Configure:
// app.UseEndpoints(endpoints =>
// {
// endpoints.MapGet("/health", async context =>
// {
// context.Response.StatusCode = 200;
// await context.Response.WriteAsync("Healthy");
// });
// });
Configuration Example (Nginx - simplified):
http {
upstream mywebapp_backend {
server 192.168.1.10:8080; # v1 Instance 1
server 192.168.1.11:8080; # v1 Instance 2
# Initially, v2 instances are not listed here
}
server {
listen 80;
server_name mywebapp.example.com;
location / {
proxy_pass http://mywebapp_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Health check endpoint for Nginx to poll
location /health {
access_log off;
return 200 "Healthy"; # Nginx will check if this returns 2xx
}
}
}
When deploying v2:
- You’d spin up new servers with
v2. - You’d update Nginx config to add
v2instances tomywebapp_backendand removev1instances, or use a more sophisticated weighted routing. - Nginx health checks would start polling the new
/healthendpoints onv2instances. - Once
v2instances are healthy, Nginx starts routing traffic to them.
The Mental Model: You’re not updating anything in place. You’re replacing entire running systems. The load balancer is the traffic cop, only sending cars to a road that’s confirmed to be clear and ready.
The Counterintuitive Lever: Most developers think about application state and how to preserve it during a deploy. The zero-downtime mindset shifts this: application instances are stateless and disposable. If an instance does have state, it should be externalized (database, cache, message queue) and accessible by all versions of the application running concurrently. This is why databases often need careful schema migration strategies that are backward-compatible with the old application version while forward-compatible with the new.
The next challenge you’ll likely face is managing database schema changes alongside your application deployments without causing data corruption or downtime for your data layer.