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
- Architecture Patterns for Zero-Downtime
- Database Migration Strategies
- Implementing Feature Flags
- Load Balancer Configuration
- Monitoring and Rollback Strategies
- Session State Management
- Testing Zero-Downtime Deployments
- Common Pitfalls and How to Avoid Them
- Platform-Specific Considerations
- Real-World Implementation Example
- Conclusion
- Need Help with .NET Deployments?
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:
- Expand: Add new schema elements without removing old ones
- Migrate: Deploy application code that writes to both old and new schema
- 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
- Enable feature for 5% of users, monitor for 24 hours
- Increase to 25% if metrics are healthy
- Increase to 50%, then 100%
- 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.
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!
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.

