An active-active setup doesn’t actually offer more availability than active-passive; it just offers faster recovery.

Let’s see this in action. Imagine we have two identical web servers, web-01 and web-02, sitting behind a load balancer.

Active-Passive (Failover)

In an active-passive setup, only one server (web-01) is actively serving traffic. The other server (web-02) is on standby, ready to take over.

# /etc/nginx/nginx.conf (on load balancer)
http {
    upstream backend_servers {
        server web-01.example.com weight=100; # Primary, actively serving
        server web-02.example.com weight=0;  # Standby, not serving
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://backend_servers;
        }
    }
}

If web-01 fails, we’d manually or automatically detect this and shift the weight for web-01 to 0 and web-02 to 100.

# After web-01 fails, update on load balancer:
# (This would typically be automated by a health check system)
http {
    upstream backend_servers {
        server web-01.example.com weight=0;  # Now standby
        server web-02.example.com weight=100; # Now primary
    }
    # ... rest of config
}

The mental model here is a primary and a backup. The backup is essentially a warm body waiting for instructions. The primary problem this solves is ensuring that if the main service goes down, there’s a pre-configured, ready-to-go alternative. The internal mechanism is the load balancer’s upstream configuration, where weights dictate traffic distribution. We control availability by adjusting these weights and the health check thresholds that trigger the switch.

Active-Active

In an active-active setup, both servers (web-01 and web-02) are actively serving traffic. The load balancer distributes requests between them.

# /etc/nginx/nginx.conf (on load balancer)
http {
    upstream backend_servers {
        server web-01.example.com weight=50; # Actively serving
        server web-02.example.com weight=50; # Actively serving
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://backend_servers;
        }
    }
}

If web-01 fails, the load balancer simply stops sending traffic to it. web-02 continues serving its 50% of the traffic, and the remaining 50% that would have gone to web-01 now gets distributed among the available servers. In this case, it all goes to web-02.

The problem solved here is increasing overall capacity and providing immediate redundancy. If one server fails, the other(s) simply absorb the load without a manual switchover or a period of unavailability. The internal mechanism is the same load balancer upstream block, but with all servers having non-zero weights. The levers we control are the weights (which can also be used for capacity planning, not just failover) and the health check settings that determine when a server is considered "down" and removed from the pool.

The surprising thing about active-active is that it doesn’t inherently increase your uptime percentage compared to a well-configured active-passive system. If your active-passive failover takes 5 minutes, and your active-active system detects a failure and removes the dead server in 5 minutes, the downtime is identical. The difference is that in active-active, the remaining server(s) handle the load without interruption, whereas in active-passive, there’s a distinct "switch" event.

When you have an active-active setup with sticky sessions (e.g., sticky_cookie), a server failure can still cause user disruption. If a user’s session is tied to the failed server, the load balancer’s health check might remove the server, but the user’s next request will likely fail or be dropped until the load balancer can redirect them to a new server, potentially losing their session state.

The next concept you’ll run into is distributed state management in an active-active environment.

Want structured learning?

Take the full System Design course →