Back to CourseAdvance6 min read
Azure /

Designing for Availability and SLAs

Design for the availability you actually need, understand how composite SLAs work, and know what an SLA does not promise.

Bishal Biswas

Bishal Biswas

Self Learner

Published on: October 27, 2026
Last updated: September 7, 2026
Advance Topic

What the numbers mean

99%      3 days 15 hours downtime per year
99.9%    8 hours 46 minutes
99.95%   4 hours 23 minutes
99.99%   52 minutes
99.999%  5 minutes

Each additional nine costs roughly ten times more and is roughly ten times harder. Deciding this is a business decision, and it should be made deliberately rather than by defaulting to “as high as possible”.

Composite SLAs multiply

This is the part most people get wrong. A chain of dependencies has a lower availability than any of its parts.

Front Door        99.99%
App Service       99.95%
Azure SQL         99.99%
Storage           99.9%

Composite: 0.9999 x 0.9995 x 0.9999 x 0.999 = 99.83%

Four services each with a strong SLA give roughly 15 hours of expected downtime a year, not 52 minutes.

Adding a component always reduces composite availability, unless it is redundant. That is the argument for fewer moving parts in a critical path.

Redundancy adds, it does not multiply

Two regions in parallel, each 99.95%
Failure probability: 0.0005 x 0.0005 = 0.00000025
Combined availability: 99.999975%

Redundant components multiply the failure probability, which makes it much smaller. That is why a second region helps so dramatically, and why a single point of failure dominates everything.

The catch: the failover mechanism itself must work, and it becomes the single point of failure. An untested failover is not redundancy.

Availability zones

az vm create --name vm-shop-01 --resource-group rg-shop-prod-uks --zone 1
az appservice plan create --name plan-shop --resource-group rg-shop-prod-uks \
  --sku P1v3 --zone-redundant --number-of-workers 3
az sql db update --name shop --server sql-shop-prod \
  --resource-group rg-shop-prod-uks --zone-redundant true
az storage account create --name stshopprod --resource-group rg-shop-prod-uks \
  --sku Standard_ZRS
Zonal            pinned to one zone. Survives nothing above a rack failure.
Zone redundant   spread across zones. Survives one datacentre failing.

Zone redundancy is the cheapest meaningful availability improvement available. It usually costs a modest premium, needs no application change, and removes an entire category of failure.

Enable it at creation. Most services cannot be made zone redundant afterwards.

What the SLA does not cover

Your code being wrong
A misconfiguration you made
A dependency outside Azure
Data loss, which is a different guarantee
Anything without a zone or redundancy option enabled
Preview features
Free and Basic tiers, which often have no SLA at all

A single instance often has no SLA at all. App Service, virtual machines and many other services require multiple instances or premium storage to be covered.

The financial remedy is a service credit, typically a percentage of that service’s monthly charge. It does not compensate for lost business, which is why the SLA is a design input rather than a protection.

Designing for a target

Work backwards from the number.

99.9% (8h 46m/year)
  single region, zone redundant, good monitoring, tested backups

99.95% (4h 23m/year)
  zone redundant everywhere, no single instance anything,
  automated deployment and rollback

99.99% (52m/year)
  multi region active passive with automatic failover,
  regularly tested, no manual step in the failover path

99.999% (5m/year)
  multi region active active, no human in any recovery path,
  and a genuine engineering investment

Note that 99.99% cannot be reached with any human step in the recovery path. Fifty two minutes a year does not allow for someone waking up.

Health checks that mean something

builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "database", tags: ["ready"])
    .AddAzureBlobStorage(blobUri, credential, name: "storage", tags: ["ready"])
    .AddRedis(redisConnection, name: "cache", tags: ["ready"])
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]);

app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});
Liveness   is the process alive? Failing means restart it.
Readiness  can it serve traffic? Failing means stop routing to it.

Confusing the two causes real problems. A readiness check that fails because the database is briefly unavailable is correct. A liveness check that fails for the same reason restarts every instance, which does not help and makes recovery slower.

Keep liveness cheap and local. Put dependencies in readiness.

Failing well

builder.Services.AddHttpClient<IRecommendationClient, RecommendationClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.BackoffType = DelayBackoffType.Exponential;
        options.CircuitBreaker.FailureRatio = 0.5;
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
    });
Retry            for transient failures, with backoff and jitter
Circuit breaker  stop calling a failing dependency, and give it room to recover
Timeout          never wait indefinitely
Fallback         a degraded response beats an error
Bulkhead         limit concurrent calls, so one dependency cannot exhaust threads

Graceful degradation is worth more than availability arithmetic. A shop that cannot show recommendations but can still take orders is available. One that returns a 500 because the recommendation service is down is not.

public async Task<IReadOnlyList<Product>> GetRecommendationsAsync(int customerId)
{
    try
    {
        return await _client.GetAsync(customerId);
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "Recommendations unavailable, falling back to popular");
        return await _cache.GetPopularProductsAsync();
    }
}

Identify which dependencies are essential and which are not, and make the non essential ones fail softly.

Testing it

az webapp restart --name app-shop --resource-group rg-shop-prod-uks
az sql failover-group set-primary --name fg-shop \
  --resource-group rg-shop-prod-uks --server sql-shop-prod-ne
az network nsg rule create --nsg-name nsg-app --resource-group rg-shop-prod-uks \
  --name block-redis --priority 100 --access Deny --destination-port-ranges 6379

Azure Chaos Studio runs controlled fault injection: killing instances, adding latency, failing over databases.

An untested failover does not work. That is not pessimism, it is the consistent experience of everyone who has tried one for the first time during an incident.

Test in production if you can do it safely, and in a production like environment if you cannot.

Measuring what users experience

requests
| where timestamp > ago(30d)
| summarize
    total = count(),
    failed = countif(success == false)
  by bin(timestamp, 1h)
| extend availability = 100.0 * (total - failed) / total
| summarize
    monthlyAvailability = avg(availability),
    worstHour = min(availability)
| extend downtimeMinutes = (100 - monthlyAvailability) / 100 * 43200

Azure’s SLA measures the platform. This measures what your users actually got, which is the number that matters and is usually lower.

Track it monthly. Trend matters more than any single month.

Quick recap

  • Composite SLAs multiply, so four strong services give roughly 15 hours of downtime a year.
  • Redundant components multiply failure probability instead, which is why a second region helps so much.
  • Zone redundancy is the cheapest meaningful improvement, and must usually be set at creation.
  • Single instance deployments often have no SLA at all.
  • 99.99 percent cannot include a human step in the recovery path.
  • Keep liveness cheap and local, and put dependencies in readiness.
  • Graceful degradation is worth more than the arithmetic, and untested failover does not work.
deploy.sh

// COMPOSITE SLAs MULTIPLY, and this is what most people get wrong:
//   Front Door 99.99% x App Service 99.95% x SQL 99.99% x Storage 99.9%
//   = 99.83%, which is ROUGHLY 15 HOURS A YEAR, not 52 minutes.
// Adding a component ALWAYS reduces availability unless it is REDUNDANT.
//
// REDUNDANCY MULTIPLIES THE FAILURE PROBABILITY INSTEAD:
//   two regions at 99.95%: 0.0005 x 0.0005 = 99.999975%
// ...provided THE FAILOVER MECHANISM WORKS. An untested failover is not
// redundancy, it is a new single point of failure.
//
// WHAT THE SLA DOES NOT COVER: your code, your misconfiguration, data loss,
// preview features, free tiers, and ANYTHING WITHOUT REDUNDANCY ENABLED.
// A SINGLE INSTANCE OFTEN HAS NO SLA AT ALL. The remedy is a service
// credit, not compensation for lost business.
//
// 99.99% (52 MINUTES A YEAR) CANNOT INCLUDE A HUMAN STEP in the recovery
// path. That does not allow for someone waking up.
//
// LIVENESS vs READINESS, and confusing them causes real problems:
//   LIVENESS   is the process alive? Failing RESTARTS it.
//   READINESS  can it serve traffic? Failing STOPS ROUTING to it.
// A readiness check failing because the database blipped is CORRECT. A
// LIVENESS check failing for the same reason RESTARTS EVERY INSTANCE, which
// does not help and makes recovery slower.
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "database", tags: ["ready"])
    .AddRedis(redisConnection, name: "cache", tags: ["ready"])
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]);
app.MapHealthChecks("/healthz/live",
    new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });
app.MapHealthChecks("/healthz/ready",
    new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });
//
builder.Services.AddHttpClient<IRecommendationClient, RecommendationClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.BackoffType = DelayBackoffType.Exponential;
        // Stop calling a failing dependency and GIVE IT ROOM TO RECOVER:
        options.CircuitBreaker.FailureRatio = 0.5;
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
    });
//
// GRACEFUL DEGRADATION IS WORTH MORE THAN THE ARITHMETIC. A shop that
// cannot show recommendations but CAN TAKE ORDERS is available. One
// returning 500 because recommendations are down is not.
public async Task<IReadOnlyList<Product>> GetRecommendationsAsync(int customerId)
{
    try { return await _client.GetAsync(customerId); }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "Recommendations unavailable, falling back");
        return await _cache.GetPopularProductsAsync();   // degraded, not failed
    }
}
//
// ZONE REDUNDANCY IS THE CHEAPEST MEANINGFUL IMPROVEMENT: a modest premium,
// no application change, and an entire category of failure removed. MOST
// SERVICES CANNOT BE MADE ZONE REDUNDANT AFTERWARDS, so enable at creation.
//
// MEASURE WHAT USERS ACTUALLY GOT, which is usually lower than the
// platform SLA and is the number that matters:
//   requests | summarize total = count(), failed = countif(success == false)
//              by bin(timestamp, 1h)
//   | extend availability = 100.0 * (total - failed) / total

Was this explanation helpful?

Your feedback helps us improve the content for everyone.