Zero-Downtime Deployments for .NET Applications

Tapesh Mehta Tapesh Mehta | Published on: Feb 07, 2026 | Est. reading time: 18 minutes
Zero-Downtime Deployments for .NET Applications

In today’s digital landscape, application downtime translates directly to lost revenue, damaged reputation, and frustrated users. For .NET applications serving thousands or millions of users, even a few minutes of downtime during deployment can be catastrophic. Zero-downtime deployment strategies ensure that your application remains available to users while you roll out new features, bug fixes, or infrastructure updates.

This comprehensive guide explores proven techniques for achieving zero-downtime deployments in .NET applications, from architectural considerations to specific implementation strategies using modern tools and platforms.

Table of Contents

Understanding Zero-Downtime Deployment

Zero-downtime deployment means updating your application without making it unavailable to users. Instead of taking your application offline, updating it, and bringing it back online, you strategically deploy new versions alongside the old ones and gradually shift traffic.

The key principles include:

  • Backward Compatibility: New code must work with the existing database schema and dependencies
  • Gradual Rollout: Deploy incrementally to detect issues early
  • Health Checks: Automated verification that new instances are ready to serve traffic
  • Rollback Strategy: Quick reversion if problems occur

Architecture Patterns for Zero-Downtime

Blue-Green Deployment

Blue-green deployment maintains two identical production environments. At any time, one (blue) serves live traffic while the other (green) is idle. When deploying, you update the green environment, test it thoroughly, then switch traffic from blue to green.

Here’s how to implement blue-green deployment in Azure DevOps for .NET applications:

# Azure Pipeline for Blue-Green Deployment
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  azureSubscription: 'YourAzureSubscription'
  webAppName: 'your-app-name'

stages:
- stage: Build
  jobs:
  - job: BuildJob
    steps:
    - task: DotNetCoreCLI@2
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      inputs:
        command: 'build'
        projects: '**/*.csproj'
        arguments: '--configuration $(buildConfiguration)'
    
    - task: DotNetCoreCLI@2
      inputs:
        command: 'publish'
        publishWebProjects: true
        arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
    
    - task: PublishBuildArtifacts@1
      inputs:
        pathToPublish: '$(Build.ArtifactStagingDirectory)'
        artifactName: 'drop'

- stage: DeployToGreen
  dependsOn: Build
  jobs:
  - deployment: DeployGreenSlot
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebApp@1
            inputs:
              azureSubscription: '$(azureSubscription)'
              appName: '$(webAppName)'
              deployToSlotOrASE: true
              slotName: 'staging'  # Green environment
              package: '$(Pipeline.Workspace)/drop/**/*.zip'

- stage: SwapSlots
  dependsOn: DeployToGreen
  jobs:
  - job: SwapJob
    steps:
    - task: AzureAppServiceManage@0
      inputs:
        azureSubscription: '$(azureSubscription)'
        action: 'Swap Slots'
        webAppName: '$(webAppName)'
        sourceSlot: 'staging'
        targetSlot: 'production'

The advantage of blue-green deployment is instant rollback—simply switch traffic back to the blue environment if issues arise.

Rolling Deployment

Rolling deployment updates instances gradually. Instead of switching all traffic at once, you update a subset of servers, verify they’re healthy, then proceed to the next batch. This approach works excellently with containerized .NET applications.

Here’s a Kubernetes deployment configuration for rolling updates:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dotnet-app-deployment
spec:
  replicas: 6
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2        # Maximum additional pods during update
      maxUnavailable: 1  # Maximum pods that can be unavailable
  selector:
    matchLabels:
      app: dotnet-app
  template:
    metadata:
      labels:
        app: dotnet-app
    spec:
      containers:
      - name: dotnet-app
        image: yourregistry/dotnet-app:v2.0
        ports:
        - containerPort: 80
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health/live
            port: 80
          initialDelaySeconds: 15
          periodSeconds: 20
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Canary Deployment

Canary deployment sends a small percentage of traffic to the new version while most users continue using the stable version. If metrics look good, you gradually increase traffic to the new version.

Implementing canary deployments with Azure App Service and Traffic Manager:

// Program.cs for health check endpoints
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

// Add health checks
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy())
    .AddSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        name: "database",
        failureStatus: HealthStatus.Degraded)
    .AddCheck<CustomHealthCheck>("custom");

builder.Services.AddControllers();

var app = builder.Build();

// Readiness probe - determines if instance can receive traffic
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

// Liveness probe - determines if instance should be restarted
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false  // Only check basic responsiveness
});

app.UseRouting();
app.MapControllers();
app.Run();

// Custom health check implementation
public class CustomHealthCheck : IHealthCheck
{
    private readonly ILogger<CustomHealthCheck> _logger;
    
    public CustomHealthCheck(ILogger<CustomHealthCheck> logger)
    {
        _logger = logger;
    }
    
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // Check critical dependencies
            // Example: verify cache connection, external API availability
            var isHealthy = await VerifyDependenciesAsync();
            
            if (isHealthy)
            {
                return HealthCheckResult.Healthy("All systems operational");
            }
            
            return HealthCheckResult.Degraded("Some dependencies are slow");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Health check failed");
            return HealthCheckResult.Unhealthy("Critical failure", ex);
        }
    }
    
    private async Task<bool> VerifyDependenciesAsync()
    {
        // Implement your dependency checks here
        await Task.Delay(10); // Simulate check
        return true;
    }
}

Database Migration Strategies

Database changes are often the most challenging aspect of zero-downtime deployments. The key is ensuring database schema changes are backward compatible.

Expand-Contract Pattern

The expand-contract pattern breaks database changes into three phases:

  1. Expand: Add new schema elements without removing old ones
  2. Migrate: Deploy application code that writes to both old and new schema
  3. Contract: Remove old schema elements after all instances use the new schema

Example implementation using Entity Framework Core migrations:

// Phase 1: Expand - Add new column
public partial class AddEmailVerifiedColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Add new column with default value for existing rows
        migrationBuilder.AddColumn<bool>(
            name: "EmailVerified",
            table: "Users",
            nullable: false,
            defaultValue: false);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "EmailVerified",
            table: "Users");
    }
}

// Phase 2: Application code that handles both old and new schema
public class UserService
{
    private readonly ApplicationDbContext _context;
    
    public UserService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task UpdateUserAsync(User user)
    {
        // Write to both old and new fields during transition
        user.IsVerified = user.EmailVerified; // Old field
        user.EmailVerified = user.IsVerified; // New field
        
        await _context.SaveChangesAsync();
    }
}

// Phase 3: Contract - Remove old column (deploy after all instances updated)
public partial class RemoveOldIsVerifiedColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "IsVerified",
            table: "Users");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<bool>(
            name: "IsVerified",
            table: "Users",
            nullable: false,
            defaultValue: false);
    }
}

Safe Schema Changes

Not all schema changes require the expand-contract pattern. Here are schema changes that are inherently safe for zero-downtime:

  • Adding new tables
  • Adding nullable columns
  • Adding indexes (use ONLINE option in SQL Server)
  • Creating new stored procedures or functions

Changes that require careful handling:

  • Renaming columns (use expand-contract with dual writes)
  • Changing column types (add new column, migrate data, drop old)
  • Adding NOT NULL constraints (add as nullable first, populate, then add constraint)
  • Removing columns (stop using first, then remove in later deployment)
-- Safe: Adding index with ONLINE option (SQL Server)
CREATE NONCLUSTERED INDEX IX_Users_Email 
ON Users(Email)
WITH (ONLINE = ON);

-- Safe: Adding nullable column
ALTER TABLE Users
ADD LastLoginDate DATETIME2 NULL;

-- Unsafe: Adding NOT NULL column without default
-- This will fail if table has existing rows
-- ALTER TABLE Users ADD PhoneNumber NVARCHAR(20) NOT NULL;

-- Safe approach: Add as nullable, populate, then alter
ALTER TABLE Users ADD PhoneNumber NVARCHAR(20) NULL;
GO
UPDATE Users SET PhoneNumber = '' WHERE PhoneNumber IS NULL;
GO
ALTER TABLE Users ALTER COLUMN PhoneNumber NVARCHAR(20) NOT NULL;

Implementing Feature Flags

Feature flags (or feature toggles) allow you to deploy code to production but keep features disabled until you’re ready to release them. This decouples deployment from release, providing maximum flexibility.

Here’s a production-ready feature flag implementation using Azure App Configuration:

// Install required packages:
// dotnet add package Microsoft.Extensions.Configuration.AzureAppConfiguration
// dotnet add package Microsoft.FeatureManagement.AspNetCore

using Microsoft.FeatureManagement;

var builder = WebApplication.CreateBuilder(args);

// Connect to Azure App Configuration
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration["AppConfig:ConnectionString"])
           .UseFeatureFlags(featureFlagOptions =>
           {
               featureFlagOptions.CacheExpirationInterval = TimeSpan.FromSeconds(30);
           });
});

builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();
builder.Services.AddControllers();

var app = builder.Build();

app.UseAzureAppConfiguration();
app.UseRouting();
app.MapControllers();
app.Run();

// Controller using feature flags
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
    private readonly IFeatureManager _featureManager;
    private readonly ILogger<PaymentController> _logger;
    
    public PaymentController(
        IFeatureManager featureManager,
        ILogger<PaymentController> logger)
    {
        _featureManager = featureManager;
        _logger = logger;
    }
    
    [HttpPost("process")]
    public async Task<IActionResult> ProcessPayment([FromBody] PaymentRequest request)
    {
        // Check if new payment provider feature is enabled
        var useNewProvider = await _featureManager.IsEnabledAsync("NewPaymentProvider");
        
        if (useNewProvider)
        {
            _logger.LogInformation("Using new payment provider");
            return await ProcessWithNewProvider(request);
        }
        
        _logger.LogInformation("Using legacy payment provider");
        return await ProcessWithLegacyProvider(request);
    }
    
    private async Task<IActionResult> ProcessWithNewProvider(PaymentRequest request)
    {
        // New implementation
        return Ok(new { status = "processed", provider = "new" });
    }
    
    private async Task<IActionResult> ProcessWithLegacyProvider(PaymentRequest request)
    {
        // Existing implementation
        return Ok(new { status = "processed", provider = "legacy" });
    }
}

public record PaymentRequest(decimal Amount, string Currency);

// Percentage-based rollout using custom feature filter
public class PercentageFilter : IFeatureFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public PercentageFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var percentage = context.Parameters.GetSection("Value").Get<int>();
        var userId = _httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value;
        
        if (string.IsNullOrEmpty(userId))
        {
            return Task.FromResult(false);
        }
        
        // Deterministic hash-based assignment
        var hash = userId.GetHashCode();
        var bucket = Math.Abs(hash % 100);
        
        return Task.FromResult(bucket < percentage);
    }
}

Feature flag configuration in Azure App Configuration:

{
  "id": "NewPaymentProvider",
  "enabled": true,
  "conditions": {
    "client_filters": [
      {
        "name": "Microsoft.Percentage",
        "parameters": {
          "Value": 10
        }
      }
    ]
  }
}

Load Balancer Configuration

Load balancers play a crucial role in zero-downtime deployments by routing traffic intelligently between old and new application versions. Modern load balancers support health checks, connection draining, and weighted routing.

Azure Application Gateway Configuration

Here’s how to configure Azure Application Gateway for zero-downtime deployments:

{
  "backendAddressPools": [
    {
      "name": "bluePool",
      "backendAddresses": [
        { "ipAddress": "10.0.1.4" },
        { "ipAddress": "10.0.1.5" }
      ]
    },
    {
      "name": "greenPool",
      "backendAddresses": [
        { "ipAddress": "10.0.2.4" },
        { "ipAddress": "10.0.2.5" }
      ]
    }
  ],
  "backendHttpSettingsCollection": [
    {
      "name": "appSettings",
      "port": 80,
      "protocol": "Http",
      "cookieBasedAffinity": "Disabled",
      "requestTimeout": 30,
      "probe": {
        "id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/applicationGateways/{gatewayName}/probes/healthProbe"
      }
    }
  ],
  "probes": [
    {
      "name": "healthProbe",
      "protocol": "Http",
      "path": "/health/ready",
      "interval": 30,
      "timeout": 30,
      "unhealthyThreshold": 3,
      "pickHostNameFromBackendHttpSettings": false,
      "minServers": 0,
      "match": {
        "statusCodes": ["200-399"]
      }
    }
  ],
  "requestRoutingRules": [
    {
      "name": "rule1",
      "ruleType": "Basic",
      "httpListener": {
        "id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/applicationGateways/{gatewayName}/httpListeners/listener1"
      },
      "backendAddressPool": {
        "id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/applicationGateways/{gatewayName}/backendAddressPools/bluePool"
      },
      "backendHttpSettings": {
        "id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/applicationGateways/{gatewayName}/backendHttpSettingsCollection/appSettings"
      }
    }
  ]
}

Connection Draining

Connection draining ensures existing requests complete before taking a server out of rotation. Implement graceful shutdown in your .NET application:

// Program.cs with graceful shutdown
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHealthChecks();

// Configure Kestrel to allow graceful shutdown
builder.WebHost.ConfigureKestrel(options =>
{
    options.AddServerHeader = false;
    options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
});

var app = builder.Build();

app.UseRouting();
app.MapControllers();
app.MapHealthChecks("/health/ready");

// Register for shutdown notification
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();

lifetime.ApplicationStopping.Register(() =>
{
    Console.WriteLine("Application is stopping. Draining connections...");
    
    // Signal to load balancer that this instance is shutting down
    // by failing health checks
    app.Services.GetRequiredService<ShutdownHealthCheck>().IsShuttingDown = true;
    
    // Wait for existing requests to complete
    Thread.Sleep(TimeSpan.FromSeconds(30));
});

lifetime.ApplicationStopped.Register(() =>
{
    Console.WriteLine("Application has stopped.");
});

await app.RunAsync();

public class ShutdownHealthCheck : IHealthCheck
{
    public bool IsShuttingDown { get; set; }
    
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        if (IsShuttingDown)
        {
            return Task.FromResult(
                HealthCheckResult.Unhealthy("Application is shutting down"));
        }
        
        return Task.FromResult(HealthCheckResult.Healthy());
    }
}

Monitoring and Rollback Strategies

Successful zero-downtime deployments require comprehensive monitoring and quick rollback capabilities when issues arise. Integrating Application Insights for logging and monitoring is essential for .NET applications.

Deployment Metrics to Track

Monitor these key metrics during deployment:

  • Error Rate: HTTP 5xx responses, unhandled exceptions
  • Response Time: P50, P95, P99 latencies
  • Throughput: Requests per second
  • Resource Usage: CPU, memory, database connections
  • Business Metrics: Conversion rates, transaction success
// Deployment monitoring with Application Insights
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

public class DeploymentMonitor
{
    private readonly TelemetryClient _telemetryClient;
    private readonly ILogger<DeploymentMonitor> _logger;
    
    public DeploymentMonitor(
        TelemetryClient telemetryClient,
        ILogger<DeploymentMonitor> logger)
    {
        _telemetryClient = telemetryClient;
        _logger = logger;
    }
    
    public void TrackDeploymentStart(string version, string environment)
    {
        var deployment = new EventTelemetry("DeploymentStarted");
        deployment.Properties["Version"] = version;
        deployment.Properties["Environment"] = environment;
        deployment.Properties["Timestamp"] = DateTime.UtcNow.ToString("o");
        
        _telemetryClient.TrackEvent(deployment);
        _logger.LogInformation("Deployment started: {Version} to {Environment}", 
            version, environment);
    }
    
    public void TrackDeploymentComplete(string version, bool success)
    {
        var deployment = new EventTelemetry("DeploymentCompleted");
        deployment.Properties["Version"] = version;
        deployment.Properties["Success"] = success.ToString();
        deployment.Metrics["Duration"] = CalculateDeploymentDuration();
        
        _telemetryClient.TrackEvent(deployment);
    }
    
    public async Task<bool> ValidateDeploymentHealth(string version)
    {
        // Query Application Insights for error rates
        var errorRate = await GetErrorRateAsync(version);
        var avgResponseTime = await GetAverageResponseTimeAsync(version);
        
        // Define thresholds
        const double maxErrorRate = 0.05; // 5%
        const double maxResponseTime = 500; // 500ms
        
        if (errorRate > maxErrorRate)
        {
            _logger.LogError(
                "Error rate {ErrorRate} exceeds threshold {Threshold}",
                errorRate, maxErrorRate);
            return false;
        }
        
        if (avgResponseTime > maxResponseTime)
        {
            _logger.LogWarning(
                "Response time {ResponseTime}ms exceeds threshold {Threshold}ms",
                avgResponseTime, maxResponseTime);
            return false;
        }
        
        return true;
    }
    
    private async Task<double> GetErrorRateAsync(string version)
    {
        // Query Application Insights for error rate
        // This is a simplified example
        await Task.Delay(100);
        return 0.01; // 1% error rate
    }
    
    private async Task<double> GetAverageResponseTimeAsync(string version)
    {
        // Query Application Insights for average response time
        await Task.Delay(100);
        return 250; // 250ms
    }
    
    private double CalculateDeploymentDuration()
    {
        // Implementation to calculate deployment duration
        return 300; // 5 minutes
    }
}

Automated Rollback

Implement automated rollback when metrics exceed thresholds:

# Azure DevOps pipeline with automated rollback
stages:
- stage: Deploy
  jobs:
  - deployment: DeployProduction
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebApp@1
            inputs:
              azureSubscription: '$(azureSubscription)'
              appName: '$(webAppName)'
              package: '$(Pipeline.Workspace)/drop/**/*.zip'
          
          # Wait for deployment to stabilize
          - task: PowerShell@2
            inputs:
              targetType: 'inline'
              script: |
                Write-Host "Waiting for deployment to stabilize..."
                Start-Sleep -Seconds 60
          
          # Validate deployment health
          - task: PowerShell@2
            name: ValidateDeployment
            inputs:
              targetType: 'inline'
              script: |
                $appInsightsApiKey = "$(AppInsightsApiKey)"
                $appInsightsAppId = "$(AppInsightsAppId)"
                
                # Query for error rate in last 5 minutes
                $query = @"
                requests
                | where timestamp > ago(5m)
                | summarize 
                    total = count(),
                    errors = countif(success == false)
                | extend errorRate = errors * 100.0 / total
                "@
                
                $headers = @{
                    "x-api-key" = $appInsightsApiKey
                }
                
                $body = @{
                    query = $query
                } | ConvertTo-Json
                
                $uri = "https://api.applicationinsights.io/v1/apps/$appInsightsAppId/query"
                $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Post -Body $body -ContentType "application/json"
                
                $errorRate = $response.tables[0].rows[0][2]
                
                Write-Host "Current error rate: $errorRate%"
                
                if ($errorRate -gt 5) {
                    Write-Host "##vso[task.complete result=Failed;]Error rate exceeds threshold"
                    exit 1
                }
        
        on:
          failure:
            steps:
            - task: PowerShell@2
              inputs:
                targetType: 'inline'
                script: |
                  Write-Host "Deployment validation failed. Initiating rollback..."
            
            # Swap back to previous slot
            - task: AzureAppServiceManage@0
              inputs:
                azureSubscription: '$(azureSubscription)'
                action: 'Swap Slots'
                webAppName: '$(webAppName)'
                sourceSlot: 'production'
                targetSlot: 'staging'
            
            - task: PowerShell@2
              inputs:
                targetType: 'inline'
                script: |
                  Write-Host "Rollback completed successfully"

Session State Management

For applications with user sessions, ensuring session continuity during deployment is critical. Stateless applications handle this naturally, but if you must maintain session state, use external storage.

// Using Redis for distributed session state
using Microsoft.AspNetCore.DataProtection;
using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);

// Configure Redis for session state
var redisConnection = builder.Configuration.GetConnectionString("Redis");
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = redisConnection;
    options.InstanceName = "SessionCache_";
});

// Add session support
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SameSite = SameSiteMode.Lax;
});

// Configure data protection to use Redis
// This ensures authentication cookies work across instances
var redis = ConnectionMultiplexer.Connect(redisConnection);
builder.Services.AddDataProtection()
    .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys")
    .SetApplicationName("MyApp");

var app = builder.Build();

app.UseSession();
app.UseRouting();
app.MapControllers();

app.Run();

// Example controller using session state
[ApiController]
[Route("api/[controller]")]
public class CartController : ControllerBase
{
    [HttpPost("add")]
    public IActionResult AddToCart([FromBody] CartItem item)
    {
        // Session is stored in Redis, survives instance updates
        var cart = HttpContext.Session.GetString("Cart");
        var cartItems = string.IsNullOrEmpty(cart) 
            ? new List<CartItem>() 
            : JsonSerializer.Deserialize<List<CartItem>>(cart);
        
        cartItems.Add(item);
        HttpContext.Session.SetString("Cart", JsonSerializer.Serialize(cartItems));
        
        return Ok(new { itemCount = cartItems.Count });
    }
}

public record CartItem(int ProductId, int Quantity);

Testing Zero-Downtime Deployments

Before implementing zero-downtime deployments in production, thoroughly test your strategy. This ties into your overall testing practices for .NET applications.

Load Testing During Deployment

Use tools like Apache JMeter, k6, or Azure Load Testing to simulate traffic during deployments:

// k6 load testing script for deployment validation
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // Ramp up to 100 users
    { duration: '5m', target: 100 },  // Maintain 100 users during deployment
    { duration: '2m', target: 0 },    // Ramp down
  ],
  thresholds: {
    'http_req_duration': ['p(95)<500'],  // 95% of requests under 500ms
    'errors': ['rate<0.01'],              // Error rate below 1%
    'http_req_failed': ['rate<0.01'],    // Failed requests below 1%
  },
};

export default function() {
  const url = 'https://your-app.azurewebsites.net/api/health';
  
  const response = http.get(url);
  
  const success = check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  
  errorRate.add(!success);
  
  sleep(1);
}

Chaos Engineering

Test your deployment strategy’s resilience by intentionally introducing failures:

// Chaos middleware to simulate random failures
public class ChaosMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ChaosMiddleware> _logger;
    private readonly Random _random = new Random();
    private readonly bool _enabled;
    private readonly double _errorProbability;
    
    public ChaosMiddleware(
        RequestDelegate next,
        ILogger<ChaosMiddleware> logger,
        IConfiguration configuration)
    {
        _next = next;
        _logger = logger;
        _enabled = configuration.GetValue<bool>("Chaos:Enabled");
        _errorProbability = configuration.GetValue<double>("Chaos:ErrorProbability");
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        if (_enabled && _random.NextDouble() < _errorProbability)
        {
            _logger.LogWarning("Chaos: Injecting random error");
            
            // Randomly choose failure mode
            var failureMode = _random.Next(0, 3);
            
            switch (failureMode)
            {
                case 0:
                    // Slow response
                    await Task.Delay(TimeSpan.FromSeconds(5));
                    break;
                    
                case 1:
                    // HTTP 500 error
                    context.Response.StatusCode = 500;
                    return;
                    
                case 2:
                    // Connection timeout
                    throw new TimeoutException("Chaos: Simulated timeout");
            }
        }
        
        await _next(context);
    }
}

// appsettings.Chaos.json (use only in test environments)
{
  "Chaos": {
    "Enabled": true,
    "ErrorProbability": 0.1  // 10% of requests will fail
  }
}

Common Pitfalls and How to Avoid Them

Breaking Database Compatibility

The most common mistake is deploying application code that’s incompatible with the existing database schema. Always ensure database changes are backward compatible and deploy schema changes before application code.

Inadequate Health Checks

Health checks that only verify the application starts are insufficient. Your health checks should verify:

  • Database connectivity
  • External service availability
  • Cache connectivity
  • Critical configuration values

Skipping Deployment Testing

Never skip testing your deployment process. Practice deployments in staging environments that mirror production as closely as possible.

Ignoring Configuration Management

Configuration changes can break deployments just as easily as code changes. Use environment-specific configuration and validate it as part of health checks. Consider implementing security best practices for configuration management.

Platform-Specific Considerations

Azure App Service

Azure App Service provides built-in deployment slots that make blue-green deployments straightforward. Key features include:

  • Deployment slots with instant swapping
  • Auto-swap for continuous deployment
  • Testing in production with traffic routing
  • Slot-specific app settings

Kubernetes

Kubernetes offers sophisticated deployment strategies through its built-in controllers. Benefits include:

  • Declarative configuration
  • Automatic health checking and self-healing
  • Progressive rollouts with automatic rollback
  • Service mesh integration for advanced traffic management

On-Premises IIS

For on-premises IIS deployments, use Application Request Routing (ARR) for load balancing and implement custom deployment scripts:

# PowerShell script for zero-downtime IIS deployment
param(
    [string]$SiteName = "MyWebsite",
    [string]$PackagePath = "C:\Deployments\latest.zip"
)

# Stop taking new traffic
Write-Host "Draining connections..."
Set-WebConfigurationProperty -Filter "/system.applicationHost/sites/site[@name='$SiteName']/application[@path='/']/virtualDirectory[@path='/']" -Name "physicalPath" -Value "C:\Maintenance"

# Wait for existing connections to complete
Start-Sleep -Seconds 30

# Deploy new version
Write-Host "Deploying new version..."
Expand-Archive -Path $PackagePath -DestinationPath "C:\inetpub\wwwroot\$SiteName" -Force

# Restore traffic
Write-Host "Restoring traffic..."
Set-WebConfigurationProperty -Filter "/system.applicationHost/sites/site[@name='$SiteName']/application[@path='/']/virtualDirectory[@path='/']" -Name "physicalPath" -Value "C:\inetpub\wwwroot\$SiteName"

# Recycle application pool
Restart-WebAppPool -Name $SiteName

Write-Host "Deployment completed successfully"

Real-World Implementation Example

Let’s walk through a complete zero-downtime deployment scenario for an e-commerce application built with .NET and React:

Scenario: Adding Payment Provider

Requirements: Add a new payment provider without disrupting existing orders.

Phase 1: Database Schema Update

-- Add new column for payment provider
ALTER TABLE Orders
ADD PaymentProvider NVARCHAR(50) NULL;

-- Set default for existing rows
UPDATE Orders
SET PaymentProvider = 'Legacy'
WHERE PaymentProvider IS NULL;

Phase 2: Deploy Application with Feature Flag

// OrderService.cs
public class OrderService
{
    private readonly IFeatureManager _featureManager;
    private readonly IPaymentService _legacyPayment;
    private readonly IPaymentService _newPayment;
    
    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        var useNewProvider = await _featureManager
            .IsEnabledAsync("NewPaymentProvider");
        
        var paymentService = useNewProvider ? _newPayment : _legacyPayment;
        
        order.PaymentProvider = useNewProvider ? "NewProvider" : "Legacy";
        
        return await paymentService.ProcessAsync(order);
    }
}

Phase 3: Gradual Rollout

  1. Enable feature for 5% of users, monitor for 24 hours
  2. Increase to 25% if metrics are healthy
  3. Increase to 50%, then 100%
  4. Remove feature flag and legacy code in next deployment

Phase 4: Cleanup

-- After 100% rollout and verification period
ALTER TABLE Orders
ALTER COLUMN PaymentProvider NVARCHAR(50) NOT NULL;

Conclusion

Zero-downtime deployments are essential for modern .NET applications that demand high availability. By implementing the strategies outlined in this guide—blue-green deployments, rolling updates, canary releases, proper database migrations, feature flags, and comprehensive monitoring—you can deploy with confidence knowing your users won’t experience disruptions.

The key to success is treating deployments as a first-class concern in your architecture from day one. Invest in automation, monitoring, and testing to make deployments routine rather than risky events. Start with simpler patterns like blue-green deployments, then graduate to more sophisticated approaches as your needs grow.

Remember that zero-downtime deployment is not just about technology—it’s about building processes, testing thoroughly, and maintaining discipline in how you manage changes to your application and infrastructure.

Need Help with .NET Deployments?

At WireFuture, we specialize in building and deploying enterprise-grade .NET applications with zero-downtime deployment strategies. Our team has extensive experience implementing ASP.NET development solutions and custom software development with robust DevOps practices.

Whether you’re migrating legacy applications to modern deployment practices or building new cloud-native solutions, we can help you achieve continuous delivery without compromising availability. Contact us at +91-9925192180 to discuss your deployment challenges.

Share

clutch profile designrush wirefuture profile goodfirms wirefuture profile
🌟 Looking to Hire Software Developers? Look No Further! 🌟

Our team of software development experts is here to transform your ideas into reality. Whether it's cutting-edge applications or revamping existing systems, we've got the skills, the passion, and the tech-savvy crew to bring your projects to life. Let's build something amazing together!

Hire Now

Categories
.NET Development Angular Development JavaScript Development KnockoutJS Development NodeJS Development PHP Development Python Development React Development Software Development SQL Server Development VueJS Development All
About Author
wirefuture - founder

Tapesh Mehta

verified Verified
Expert in Software Development

Tapesh Mehta is a seasoned tech worker who has been making apps for the web, mobile devices, and desktop for over 15+ years. Tapesh knows a lot of different computer languages and frameworks. For robust web solutions, he is an expert in Asp.Net, PHP, and Python. He is also very good at making hybrid mobile apps, which use Ionic, Xamarin, and Flutter to make cross-platform user experiences that work well together. In addition, Tapesh has a lot of experience making complex desktop apps with WPF, which shows how flexible and creative he is when it comes to making software. His work is marked by a constant desire to learn and change.

Get in Touch
Your Ideas, Our Strategy – Let's Connect.

No commitment required. Whether you’re a charity, business, start-up or you just have an idea – we’re happy to talk through your project.

Embrace a worry-free experience as we proactively update, secure, and optimize your software, enabling you to focus on what matters most – driving innovation and achieving your business goals.

Hire Your A-Team Here to Unlock Potential & Drive Results
You can send an email to contact@wirefuture.com
clutch wirefuture profile designrush wirefuture profile goodfirms wirefuture profile good firms award-4 award-5 award-6