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

  1. 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.
  2. 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.
  3. 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.

  1. Deploy v2 to New Instances:

    • Provision new infrastructure (e.g., new VMs, new containers).
    • Deploy v2 of 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.
  2. 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 /health endpoint. For v2, it should return 200 OK only when it’s fully ready, all dependencies are available, and it’s ready to serve traffic.
    • The load balancer will continuously poll /health on the v2 instances. Once they pass the health check, they are considered "ready."
  3. Shift Traffic Gradually (Canary Deployment):

    • Configure the load balancer to send a small percentage of new traffic (e.g., 5%) to the v2 instances. The remaining 95% still goes to v1.
    • Monitor application metrics (error rates, latency, resource utilization) for both v1 and v2 instances. If v2 shows any issues, you can immediately revert by removing it from the load balancer’s pool.
  4. Shift More Traffic:

    • If v2 is stable, gradually increase the traffic percentage to it (e.g., 25%, 50%, 75%) while decreasing traffic to v1.
    • Continue monitoring.
  5. Full Cutover:

    • Once 100% of traffic is successfully routed to v2 and it’s performing well, the v1 instances are no longer receiving user requests.
  6. Decommission Old Instances:

    • Now you can safely shut down or remove the old v1 instances.

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:

  1. You’d spin up new servers with v2.
  2. You’d update Nginx config to add v2 instances to mywebapp_backend and remove v1 instances, or use a more sophisticated weighted routing.
  3. Nginx health checks would start polling the new /health endpoints on v2 instances.
  4. Once v2 instances 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.

Want structured learning?

Take the full Csharp course →