Exercises
Objectives
By the end of this lesson, you should be able to:
- Extend a load balancer to check each instance’s health before routing to it
- Confirm an unhealthy instance is automatically excluded from rotation
- Explain why health checks are essential once instances can fail independently
⚠️ A note on verification: every command and every output in this lesson was actually run.
Exercise: A Health-Aware Load Balancer
a) Instances that can report themselves unhealthy (simulating a real instance failing, for example, losing its database connection):
function createInstance(name, port, { healthy = true } = {}) {
const app = express();
let isHealthy = healthy;
app.get('/api/v1/health', (req, res) => {
if (!isHealthy) return res.status(503).json({ instance: name, status: 'unhealthy' });
res.json({ instance: name, status: 'ok' });
});
app.get('/api/v1/data', (req, res) => res.json({ instance: name, data: 'some response' }));
const server = app.listen(port, () => console.log(`Instance ${name} listening on ${port}`));
return { setHealthy: (v) => { isHealthy = v; }, server };
}
b) A load balancer that polls each instance’s health, and only routes to healthy ones:
function createHealthAwareLoadBalancer(targets, port, { checkIntervalMs = 100 } = {}) {
const proxy = httpProxy.createProxyServer({});
const status = new Map(targets.map((t) => [t, true]));
let i = 0;
async function checkHealth() {
for (const target of targets) {
try {
const res = await fetch(`${target}/api/v1/health`);
status.set(target, res.ok);
} catch {
status.set(target, false);
}
}
}
setInterval(checkHealth, checkIntervalMs);
const server = http.createServer((req, res) => {
const healthyTargets = targets.filter((t) => status.get(t));
if (healthyTargets.length === 0) {
res.writeHead(503);
return res.end('No healthy instances');
}
const target = healthyTargets[i % healthyTargets.length];
i++;
proxy.web(req, res, { target });
});
server.listen(port, () => console.log(`Health-aware load balancer on ${port}`));
}
The load balancer polls /api/v1/health on every target, on its own schedule, independent of actual traffic, healthyTargets is filtered fresh on every incoming request, only cycling through instances that passed their most recent check.
c) Confirm normal, even distribution across all three instances:
--- all instances healthy ---
[ 'A', 'B', 'C', 'A', 'B', 'C' ]
d) Mark one instance unhealthy, and confirm it’s automatically excluded:
--- marking instance B unhealthy ---
--- requests after B goes unhealthy ---
[ 'A', 'C', 'A', 'C', 'A', 'C' ]
Instance B never appears again, once its health check started failing, no request was ever routed to it, this is what makes horizontal scaling actually resilient, not just distributing load across healthy instances, but automatically routing around one that’s stopped being healthy, without a person needing to notice and intervene, echoing Module 2’s PM2 automatic restarts, but at the load-balancing layer instead of the process layer.
e) Extend it. Mark instance B healthy again (b.setHealthy(true)), wait for the next health check interval, and confirm it rejoins the rotation automatically.
f) Reflect. Explain, in one or two sentences, what would happen to real user requests if this load balancer had no health checks at all, and one instance genuinely crashed or became unresponsive.
Recap
This module took Module 2’s single-machine process management and extended it across multiple, independent instances: horizontal scaling with no hard ceiling the way vertical scaling has, statelessness as the property that makes it safe (JWTs working correctly across instances where sessions don’t), and a load balancer, confirmed here to both distribute load evenly and automatically route around an unhealthy instance, without anyone needing to intervene by hand.
Next module: observability, actually finding out what a running, scaled, multi-instance application is doing, in real time.