Skip to Content
Course content

86: Building a Simple Load Balancer in Go

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Wait, do I have to manually copy every HTTP header to the backend?

You might be tempted to use http.Get to fetch data from a backend and then write that response back to the client. Please, don't do that. You'll spend your entire weekend trying to figure out why your cookies are disappearing or why the Content-Type headers are slightly off.

In Go, the net/http/httputil package gives us ReverseProxy. It's a powerhouse. It takes an incoming request, modifies it slightly, sends it to the target, and streams the response back to the client. It handles the heavy lifting of header copying and connection management for you.

package main

import (
	"net/http"
	"net/http/httputil"
	"net/url"
)

func proxyRequest(target string, w http.ResponseWriter, r *http.Request) {
	remote, _ := url.Parse(target)
	proxy := httputil.NewSingleHostReverseProxy(remote)
	
	// The proxy handles the request and writes the response to 'w'
	proxy.ServeHTTP(w, r)
}

I've used this pattern in production dozens of times. It's clean, efficient, and lets you focus on the logic of where to send the traffic, rather than how to move the bytes.

How do I make sure the requests are spread evenly?

The simplest way to handle this is "Round Robin." You keep a list of your backend servers and an index. Every time a request comes in, you send it to the current index and then increment it.

But here's the catch: since your load balancer will be handling many requests concurrently in different goroutines, you can't just use i++. That's a race condition waiting to happen. I prefer using the sync/atomic package for this because it's significantly faster than a mutex for a simple counter.

import (
	"net/http"
	"sync/atomic"
)

type LoadBalancer struct {
	targets []string
	current uint64
}

func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Atomically increment and get the index
	idx := atomic.AddUint64(&lb.current, 1)
	target := lb.targets[idx%uint64(len(lb.targets))]
	
	// Now we use the proxy logic from before
	remote, _ := url.Parse(target)
	proxy := httputil.NewSingleHostReverseProxy(remote)
	proxy.ServeHTTP(w, r)
}

What happens if one of my backend servers dies?

If you use the code above and one server goes down, your load balancer will keep sending 1/3 of your traffic (assuming three servers) into a black hole. That's a bad user experience. You need a way to "mark" servers as unhealthy.

The most robust way is to run a background goroutine that pings each server every few seconds. I usually create a Backend struct that tracks the status. If the health check fails, we flip a boolean and the ServeHTTP method skips that server.

type Backend struct {
	URL          *url.URL
	Alive        bool
	mux          sync.RWMutex
	ReverseProxy *httputil.ReverseProxy
}

func (b *Backend) SetAlive(alive bool) {
	b.mux.Lock()
	b.Alive = alive
	b.mux.Unlock()
}

func (b *Backend) IsAlive() bool {
	b.mux.RLock()
	defer b.mux.RUnlock()
	return b.Alive
}

// In your health check loop:
func healthCheck() {
	for _, b := range backends {
		res, err := http.Get(b.URL.String())
		if err != nil || res.StatusCode != http.StatusOK {
			b.SetAlive(false)
		} else {
			b.SetAlive(true)
		}
	}
}

One tip: keep your health check timeout short. You don't want your monitoring routine hanging for 30 seconds while your users are getting 502 errors.




📋 Practical Task

Implementing a Dynamic Server Registry for the Load Balancer

Currently, our load balancer uses a hardcoded list of servers. In a real environment, servers spin up and down constantly.

Your Task: Modify the LoadBalancer struct to include a method called AddBackend(url string) and RemoveBackend(url string). These methods must be thread-safe (use a sync.RWMutex) so that you can add or remove backend servers from the pool while the load balancer is actively routing live traffic without causing a panic.

Verify your implementation by starting the balancer, adding a backend, sending a few requests, removing that backend, and ensuring the balancer gracefully switches to the remaining servers.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.