Full Stack • Java • System Design • Cloud • AI Engineering

High Availability

A complete guide to high availability in production engineering — covering availability math, failure modes, redundancy patterns, load balancing, health checks, auto-scaling, database HA, multi-region architecture, and the readiness checklist every production system needs.

Introduction

High availability means the system is operational and serving requests when users need it.

Not sometimes. Not most of the time. Reliably, measurably, and provably close to all of the time.

A system that is down 8 hours a month is not production-ready, regardless of how fast it runs when it is up. Availability is a first-class system property that must be designed for deliberately — it does not emerge from writing good code alone.

High Availability (HA) is the discipline of eliminating or tolerating every category of failure that would cause the system to stop serving users. This means redundancy at every layer, fast failure detection, automatic recovery, and load distribution that prevents any single component from being a bottleneck or a single point of failure.


Availability Math

Availability is expressed as a percentage of time the system is operational over a measurement period.

Availability = (Uptime / (Uptime + Downtime)) × 100

The industry shorthand is "nines":

Availability Downtime per Year Downtime per Month Downtime per Week
99% 3.65 days 7.2 hours 1.68 hours
99.9% 8.76 hours 43.8 minutes 10.1 minutes
99.95% 4.38 hours 21.9 minutes 5.04 minutes
99.99% 52.6 minutes 4.38 minutes 1.01 minutes
99.999% 5.26 minutes 26.3 seconds 6.05 seconds

Moving from 99.9% to 99.99% reduces allowable downtime from 43 minutes per month to 4 minutes per month. That is a 10× improvement, and it requires a fundamentally different architecture — not just better code.

Composite Availability

When multiple components sit in series (each must be up for the system to work), availability multiplies:

System Availability = Component_A × Component_B × Component_C

A chain of three components each at 99.9% availability gives:

0.999 × 0.999 × 0.999 = 0.997 = 99.7%

Three 99.9% components in series produce a system at only 99.7%. This is why eliminating single points of failure and adding redundancy is essential — the math punishes long chains of unredundant dependencies.


Categories of Failure

High availability design starts by enumerating every failure mode. You cannot design against failures you have not named.

mindmap
  root((Failure Modes))
    Hardware
      Server crash
      Disk failure
      Network card failure
      Power supply failure
    Software
      Application crash
      Memory leak
      Deadlock
      Unhandled exception
    Infrastructure
      Cloud availability zone outage
      Load balancer failure
      DNS failure
      Network partition
    Dependency
      Database unavailable
      Cache miss or failure
      Downstream API timeout
      Message queue full
    Operational
      Bad deployment
      Configuration error
      Capacity exhaustion
      Certificate expiry
    Human
      Accidental deletion
      Misconfigured firewall
      Failed migration
      Operator error during incident

Each failure mode requires a specific mitigation. No single technique covers all categories.


The Pillars of High Availability

High availability is built on five foundational practices, applied at every layer of the system.

flowchart TB
    HA["High Availability"]
    HA --> R["Redundancy\nEliminate single points\nof failure"]
    HA --> FD["Failure Detection\nKnow when something\nhas failed"]
    HA --> FR["Failover\nRoute around\nfailed components"]
    HA --> LB["Load Distribution\nSpread load, prevent\nbottlenecks"]
    HA --> Recovery["Self-Healing\nRecover automatically\nwithout human intervention"]

Redundancy Patterns

N+1 Redundancy

The minimum viable HA pattern. For every component you need N of to serve capacity, provision N+1. The extra instance absorbs traffic if any one instance fails.

flowchart LR
    LB["Load Balancer"]
    A["App Instance 1\n(Active)"]
    B["App Instance 2\n(Active)"]
    C["App Instance 3\n(Standby / N+1)"]

    LB --> A
    LB --> B
    LB -.->|"Takes over on failure"| C

N+1 is the floor. It handles single-instance failure but does not protect against zone-level outages or correlated failures.

2N Redundancy (Active-Passive)

Two complete copies of the system. One active, one idle. On failure of the active, the passive becomes active.

Property Value
Redundancy Full duplicate capacity
Cost 2× infrastructure cost
Failover time Seconds to minutes (depends on automation)
Best for Databases, stateful components

2N+1 Redundancy (Quorum)

Used when a decision must be made between distributed nodes (leader election, consensus). An odd number of nodes ensures a majority is always achievable.

flowchart TB
    subgraph Quorum["3 Nodes — Quorum = 2"]
        N1["Node 1\n(Leader)"]
        N2["Node 2\n(Follower)"]
        N3["Node 3\n(Follower)"]
    end

    N1 <--> N2
    N2 <--> N3
    N1 <--> N3

    Failure["Node 1 fails"] --> Election["Nodes 2 and 3\nelect new leader\n(Quorum achieved: 2/3)"]

With 3 nodes, one failure is tolerated. With 5 nodes, two failures are tolerated.

Geographic Redundancy

Redundancy distributed across multiple availability zones or regions. Protects against infrastructure-level failures that affect an entire datacenter.

flowchart TB
    DNS["Global Load Balancer"]

    subgraph AZ_A["Availability Zone A"]
        App_A1["App Instance 1"]
        App_A2["App Instance 2"]
    end

    subgraph AZ_B["Availability Zone B"]
        App_B1["App Instance 3"]
        App_B2["App Instance 4"]
    end

    subgraph AZ_C["Availability Zone C"]
        App_C1["App Instance 5"]
        App_C2["App Instance 6"]
    end

    DNS --> AZ_A
    DNS --> AZ_B
    DNS --> AZ_C

The rule: always deploy across at least 2 availability zones. Deploy across 3 for mission-critical systems.


Load Balancing

A load balancer distributes incoming requests across multiple backend instances. It is both a redundancy mechanism and a failure detection mechanism.

Load Balancing Algorithms

Algorithm How It Works Best For
Round Robin Requests distributed in rotation Homogeneous instances, stateless
Least Connections Routes to instance with fewest active connections Long-lived connections, WebSockets
IP Hash Consistent routing based on client IP Session affinity requirements
Weighted Round Robin Round robin with capacity-based weights Heterogeneous instance sizes
Random Randomly chosen backend Simple stateless services

Layer 4 vs Layer 7 Load Balancing

flowchart TB
    subgraph L4["Layer 4 (TCP/UDP)"]
        L4_Client["Client"]
        L4_LB["L4 Load Balancer\n(Routes by IP + port)"]
        L4_B1["Backend 1"]
        L4_B2["Backend 2"]
        L4_Client --> L4_LB --> L4_B1
        L4_LB --> L4_B2
    end

    subgraph L7["Layer 7 (HTTP/HTTPS)"]
        L7_Client["Client"]
        L7_LB["L7 Load Balancer\n(Routes by URL, headers,\ncookies, content type)"]
        L7_API["API Service"]
        L7_Static["Static Assets"]
        L7_WS["WebSocket Service"]
        L7_Client --> L7_LB
        L7_LB -->|"/api/*"| L7_API
        L7_LB -->|"/static/*"| L7_Static
        L7_LB -->|"/ws/*"| L7_WS
    end
Property Layer 4 Layer 7
Routing basis IP, port, TCP/UDP URL, headers, cookies, body
SSL termination Pass-through Terminates SSL, inspects content
Content routing Not possible Route by path, host, header
Performance Faster — less processing Slightly slower — more processing
Health checks TCP connection HTTP endpoint check

Most production systems use a Layer 7 load balancer at the edge and Layer 4 internally.

Health Checks and Instance Removal

A load balancer continuously probes each backend. When a backend fails its health check, the load balancer removes it from rotation automatically.

sequenceDiagram
    participant LB as Load Balancer
    participant Healthy as Backend 1 (Healthy)
    participant Failed as Backend 2 (Failed)

    loop Every 10 seconds
        LB->>Healthy: GET /health
        Healthy-->>LB: 200 OK
        LB->>Failed: GET /health
        Failed-->>LB: Connection timeout
    end

    Note over LB: Backend 2 fails 3 consecutive checks
    LB->>LB: Remove Backend 2 from pool
    Note over LB: All traffic now routes to Backend 1
    LB-->>Failed: Still probing (for recovery detection)

    Failed-->>LB: 200 OK (recovered)
    LB->>LB: Re-add Backend 2 to pool

Key parameters:

  • Check interval — how frequently the load balancer probes (commonly 10–30 seconds)
  • Healthy threshold — consecutive successes before marking healthy (typically 2–3)
  • Unhealthy threshold — consecutive failures before marking unhealthy (typically 2–3)
  • Timeout — how long to wait for a response (commonly 5 seconds)

Health Checks

Health checks are the mechanism by which infrastructure determines whether a component is ready to serve traffic. A component can be running but not healthy — a misconfigured database connection, an exhausted thread pool, or a broken dependency can all make an instance unable to serve requests correctly.

Liveness vs Readiness

Check Type Question It Answers Action on Failure
Liveness Is this instance alive at all? Restart the instance
Readiness Is this instance ready to accept traffic? Remove from load balancer pool
Startup Has the instance finished initializing? Don't route traffic until startup passes

A liveness check that is too aggressive will restart healthy instances. A readiness check that is too permissive routes traffic to broken instances. Both endpoints must be implemented correctly.

Deep Health Check

A shallow health check (200 OK on any request) is nearly useless. A deep health check validates that the instance's dependencies are functional:

# Example deep health check response
GET /health

{
  "status": "healthy",
  "checks": {
    "database": {
      "status": "healthy",
      "latency_ms": 3
    },
    "redis": {
      "status": "healthy",
      "latency_ms": 1
    },
    "downstream_api": {
      "status": "degraded",
      "latency_ms": 450,
      "message": "Elevated latency"
    }
  },
  "uptime_seconds": 86400
}

Return 200 when all critical dependencies are healthy. Return 503 when a critical dependency is down. Return 200 with a degraded status for non-critical issues — so traffic is not removed but operators are alerted.


Auto-Scaling

Static capacity sizing is an HA anti-pattern. A fixed fleet that is sized for average load will be overwhelmed by peak load and over-provisioned during off-peak hours. Auto-scaling adjusts capacity dynamically based on demand.

Horizontal vs Vertical Scaling

flowchart LR
    subgraph Horizontal["Horizontal Scaling (Scale Out/In)"]
        H1["Add more instances"]
        H2["Each instance handles\na fraction of total load"]
        H3["Preferred for HA —\nno single point of failure"]
    end

    subgraph Vertical["Vertical Scaling (Scale Up/Down)"]
        V1["Larger instances\n(more CPU, RAM)"]
        V2["One instance handles\nmore load"]
        V3["Single point of failure\nremains"]
    end

Horizontal scaling is preferred for high availability because it distributes failure surface. Vertical scaling has a ceiling and still leaves a single point of failure.

Auto-Scaling Triggers

Trigger Metric Example Threshold
CPU utilization Average CPU % across fleet Scale out if CPU > 70% for 3 min
Memory utilization Average memory % across fleet Scale out if memory > 80%
Request rate Requests per second Scale out if RPS > 1000
Queue depth Messages waiting in queue Scale out if queue > 500
Response latency p99 latency exceeds threshold Scale out if p99 > 500ms
Custom metric Application-defined business metric Scale on orders/second

Scale-Out vs Scale-In Asymmetry

Scale out aggressively. Scale in conservatively.

A system that scales out too slowly leaves users experiencing degraded performance. A system that scales in too aggressively causes thrashing (constant add/remove cycles) and can leave the fleet under-provisioned if load rebounds.

flowchart LR
    subgraph ScaleOut["Scale-Out Policy"]
        SO1["Threshold breached\n(CPU > 70%)"]
        SO2["Wait 1 minute\n(short cooldown)"]
        SO3["Add 2 instances"]
    end

    subgraph ScaleIn["Scale-In Policy"]
        SI1["Threshold sustained low\n(CPU < 30%)"]
        SI2["Wait 15 minutes\n(long cooldown)"]
        SI3["Remove 1 instance"]
    end

Common practice: scale out after 1–3 minutes of elevated load, scale in after 10–15 minutes of sustained low load.

Predictive Scaling

For predictable traffic patterns (daily peaks, scheduled batch jobs, planned events), pre-scale the fleet before load arrives rather than waiting for auto-scaling to react.


Database High Availability

Databases are the most common single point of failure in production systems. A stateless application tier can be replaced in seconds. A database failure that causes data loss or corruption is catastrophic.

Primary-Replica Architecture

The foundation of database HA. Writes go to the primary. Reads can be served by replicas. If the primary fails, a replica is promoted.

flowchart TB
    App["Application"]
    Primary["Primary DB\n(Accepts reads + writes)"]
    Replica1["Read Replica 1\n(Read-only)"]
    Replica2["Read Replica 2\n(Read-only)"]
    Replica3["Standby Replica\n(Failover candidate)"]

    App -->|"Writes"| Primary
    App -->|"Reads"| Replica1
    App -->|"Reads"| Replica2

    Primary -->|"Async replication"| Replica1
    Primary -->|"Async replication"| Replica2
    Primary -->|"Sync replication"| Replica3

    Failure["Primary fails"] --> Promote["Promote Standby Replica\nto Primary"]
Replication Type Consistency Latency impact Data loss risk
Synchronous Strong — no lag Higher write latency None (zero RPO)
Asynchronous Eventual — some lag No write latency overhead Seconds of data loss on failure

Connection Pooling

Direct database connections are expensive. Connection pools maintain a set of pre-established connections that the application reuses. This prevents connection exhaustion under load and reduces connection setup latency.

flowchart LR
    App1["App Instance 1"]
    App2["App Instance 2"]
    App3["App Instance 3"]

    Pool["Connection Pool\n(PgBouncer / ProxySQL)\nMax 20 DB connections"]

    DB["Database\n(Max 100 connections)"]

    App1 -->|"Up to 100 connections"| Pool
    App2 -->|"Up to 100 connections"| Pool
    App3 -->|"Up to 100 connections"| Pool
    Pool -->|"20 pooled connections"| DB

Without a connection pool, 100 application instances each opening 10 connections would saturate a database at its connection limit.

Multi-AZ Database Deployment

Cloud providers offer managed multi-AZ database deployments (AWS RDS Multi-AZ, Google Cloud SQL HA, Azure SQL Zone-Redundant). The database is automatically replicated to a standby in a different availability zone. Failover to the standby is automatic and typically completes in 60–120 seconds.

flowchart TB
    subgraph AZ_A["Availability Zone A"]
        Primary_DB["Primary DB\n(Active — accepts all traffic)"]
    end

    subgraph AZ_B["Availability Zone B"]
        Standby_DB["Standby DB\n(Passive — synchronous replica)"]
    end

    Primary_DB -->|"Synchronous replication"| Standby_DB

    Failure["AZ-A failure"] --> Failover["Automatic failover\nStandby promoted to primary\nDNS endpoint updated\n~60–120 seconds"]
    Failover --> Standby_DB

Circuit Breaker Pattern

A circuit breaker prevents cascading failures by stopping requests to a failing dependency before those failures accumulate and overwhelm the system.

Without a circuit breaker, requests to a slow or failing dependency pile up. Threads are consumed waiting for responses. The application's own thread pool is exhausted. The application itself becomes unavailable — not because of its own failure, but because of a dependency's.

Circuit Breaker State Machine

stateDiagram-v2
    [*] --> Closed

    Closed --> Open: Failure rate exceeds threshold\n(e.g., 50% of last 10 requests failed)
    Open --> HalfOpen: Timeout expires\n(e.g., 30 seconds)
    HalfOpen --> Closed: Test request succeeds
    HalfOpen --> Open: Test request fails
State Behavior
Closed Requests pass through normally. Failures are counted.
Open Requests fail immediately without contacting the dependency.
Half-Open One test request is allowed through to check if the dependency has recovered.

The circuit breaker fails fast when the dependency is down — returning an error in microseconds instead of blocking for 30 seconds waiting for a timeout.


Graceful Degradation

When a dependency fails, the system should degrade gracefully rather than fail completely.

Degradation Strategy Description Example
Fallback response Return a cached, default, or reduced response Return cached product list when DB is slow
Feature flag off Disable the feature that depends on the failing dependency Hide recommendation engine when ML service is down
Queue and retry Accept the request, process it later when the dependency recovers Queue writes when downstream API is unavailable
Read-only mode Accept reads, reject writes until dependency recovers Display existing data, disable mutations
Static fallback Serve pre-rendered static content Show cached homepage instead of dynamic content

The goal is to keep the system partially useful rather than completely broken.


Multi-Region High Availability

Single-region deployments are subject to region-level outages. For systems requiring 99.99% availability or higher, multi-region deployment is necessary.

Active-Passive Multi-Region

One region serves all production traffic. The second region is a hot standby, ready to receive traffic on failover.

flowchart TB
    DNS["Global Load Balancer / DNS"]

    subgraph Primary["Primary Region — us-east-1 (100% traffic)"]
        P_LB["Load Balancer"]
        P_App["Application Fleet"]
        P_DB["Primary Database"]
    end

    subgraph DR["DR Region — us-west-2 (0% traffic)"]
        D_LB["Load Balancer"]
        D_App["Application Fleet (scaled down)"]
        D_DB["Replica Database"]
    end

    DNS -->|"100%"| P_LB
    DNS -.->|"0% — ready"| D_LB

    P_DB -->|"Continuous replication"| D_DB

    Failure["us-east-1 outage"] --> Cutover["DNS failover\n~1–5 minutes"]
    Cutover --> D_LB

Active-Active Multi-Region

All regions simultaneously serve live traffic. No manual failover required. If a region fails, the global load balancer routes its traffic to the remaining healthy regions.

flowchart TB
    DNS["Global Load Balancer\n(Anycast / Route 53 / Cloudflare)"]

    subgraph US["us-east-1 (35%)"]
        US_App["Application"]
        US_DB["Database (Primary)"]
    end

    subgraph EU["eu-west-1 (35%)"]
        EU_App["Application"]
        EU_DB["Database (Primary)"]
    end

    subgraph APAC["ap-southeast-1 (30%)"]
        APAC_App["Application"]
        APAC_DB["Database (Primary)"]
    end

    DNS --> US
    DNS --> EU
    DNS --> APAC

    US_DB <-->|"Bidirectional replication"| EU_DB
    EU_DB <-->|"Bidirectional replication"| APAC_DB
    US_DB <-->|"Bidirectional replication"| APAC_DB

Active-active eliminates the need for failover but introduces complexity:

  • Write conflicts — two regions writing the same record simultaneously require conflict resolution (last-write-wins, CRDTs, or application-level resolution)
  • Replication lag — users may temporarily see stale data immediately after a cross-region write
  • Cost — full production capacity in every region

Active-active is the right choice when downtime cost exceeds the infrastructure cost, or when global latency requirements demand regional proximity.


Zero-Downtime Deployments

A deployment that takes down the application is a planned availability event. Every deployment strategy must be evaluated against its availability impact.

Strategy Downtime Rollback Speed Traffic during deploy
Recreate Yes — full Deploy old version Zero — service interrupted
Rolling No Slow — roll back all instances Mixed versions in flight
Blue-Green No Instant — flip traffic back 100% on one color
Canary No Instant — set canary weight to 0 Split by percentage

For a highly available system, any deployment strategy that requires downtime is unacceptable.


Observability for High Availability

A system that is unavailable but doesn't know it is available in name only. Observability is the prerequisite for high availability.

The Four Golden Signals for Availability

Signal What to Measure Availability Implication
Latency p50, p95, p99 response time High latency is often the first sign of failure
Traffic Requests per second Unexpected drops in traffic may indicate an unreachable service
Errors Error rate (5xx / total requests) Rising error rate precedes full unavailability
Saturation CPU, memory, queue depth, connection pool usage Saturation predicts imminent unavailability

Alerting Thresholds

Metric Warning Critical
Error rate > 1% > 5%
p99 latency > 500ms > 2000ms
CPU utilization > 70% sustained > 90% sustained
Memory utilization > 80% > 95%
DB connection pool usage > 75% > 90%
Health check failures Any failure 2+ consecutive failures

SLA, SLO, and SLI

Availability commitments require precise definitions.

Term Definition Example
SLA Service Level Agreement — contractual commitment with consequences 99.9% monthly uptime or credits issued
SLO Service Level Objective — internal target, more ambitious than SLA 99.95% monthly uptime target (internal)
SLI Service Level Indicator — the actual measured metric 99.97% uptime measured last 30 days

The relationship:

SLI (measured) ≥ SLO (internal target) ≥ SLA (customer commitment)

Set internal SLOs above the SLA commitment to create a buffer. If the SLO is breached, you have time to fix the problem before the SLA is breached. If the SLA is breached first, it is already a customer-impacting failure.

Error Budget

An error budget is the allowable downtime derived from an SLO. It makes availability concrete and tradeable.

Error Budget = 100% - SLO
For SLO of 99.9%: Error Budget = 0.1% = 43.8 minutes/month

When the error budget is consumed, the engineering team stops new feature deployments and focuses exclusively on reliability until the budget is replenished at the start of the next period.


High Availability Checklist

Redundancy

  • No single points of failure exist at any layer (compute, load balancer, database, DNS)
  • Application tier deployed across at least 2 availability zones
  • Database deployed in multi-AZ configuration with automatic failover
  • Load balancer itself is redundant (managed LB or multiple instances)

Load Balancing

  • Load balancer health checks are configured for all backends
  • Unhealthy backends are automatically removed from rotation
  • Load balancing algorithm is appropriate for workload type
  • Connection draining is enabled (in-flight requests complete before instance removal)

Health Checks

  • Liveness and readiness endpoints are implemented and distinct
  • Health checks validate critical dependencies (database, cache, downstream APIs)
  • Health check thresholds are tuned (not too sensitive, not too permissive)

Auto-Scaling

  • Auto-scaling is configured for the application tier
  • Scale-out triggers are set conservatively (scale before saturation)
  • Scale-in cooldown is long enough to prevent thrashing
  • Minimum instance count is set to survive one instance failure (minimum 2)

Database

  • Primary-replica setup with automated failover is configured
  • Read replicas are used to offload read traffic
  • Connection pooling is configured between application and database
  • Database backups are configured and tested

Failure Isolation

  • Circuit breakers are implemented for all external dependencies
  • Timeouts are configured on all external calls (no infinite waits)
  • Retry logic with exponential backoff is implemented
  • Graceful degradation paths exist for non-critical dependencies

Deployments

  • All deployments use zero-downtime strategies (rolling, blue-green, canary)
  • Deployment health gates validate availability before proceeding
  • Automated rollback is configured if health checks fail post-deploy

Observability

  • The four golden signals are monitored and alerted
  • Availability SLOs are defined, measured, and visible
  • Error budgets are tracked and acted upon when consumed
  • On-call rotation and escalation paths are defined

Summary

High availability is not a single feature — it is a cross-cutting concern that must be designed in at every layer of the system.

flowchart TB
    Goal["99.99% Availability Goal"]

    Goal --> Redundancy["Redundancy\nN+1 instances\nMulti-AZ\nMulti-region"]
    Goal --> LB["Load Balancing\nHealth checks\nAutomatic removal\nConnection draining"]
    Goal --> AS["Auto-Scaling\nScale before saturation\nMinimum instance count\nPredictive scaling"]
    Goal --> DB_HA["Database HA\nPrimary-replica\nMulti-AZ failover\nConnection pooling"]
    Goal --> Isolation["Failure Isolation\nCircuit breakers\nTimeouts + retries\nGraceful degradation"]
    Goal --> Observability["Observability\nGolden signals\nSLO tracking\nError budgets"]

The progression of HA maturity:

Maturity Level Characteristics Typical Availability
Level 1 Single instance, manual recovery 99% – 99.5%
Level 2 Multiple instances, basic load balancing 99.5% – 99.9%
Level 3 Multi-AZ, auto-scaling, health checks, DB failover 99.9% – 99.95%
Level 4 Multi-region active-passive, circuit breakers, zero-downtime deploys 99.95% – 99.99%
Level 5 Multi-region active-active, chaos engineering, error budget discipline 99.99%+

Start by eliminating the obvious single points of failure. Add redundancy layer by layer. Measure availability precisely using SLIs. Set SLOs that exceed your SLA commitments. Build the culture of reliability around error budgets — when the budget is consumed, reliability work takes priority over feature work.

A system that is reliably available is a system that users trust. Availability is not a technical metric. It is a user experience.


This page is part of the Production Engineering learning path.

Return to the System Design Learning Path to continue with the complete roadmap.