Huy Phan

#001

Tech

#001


Load Balancing in Go

Intro

I’ve always had a natural curiosity about how things worked under the hood and the only way I can truly make sense of things is to build them. Theory will only take you so far… To truly understand systems and their components, I like to roll up my sleeves and build something real. This project not only deepened my understanding of load balancing but also gave me hands-on experience with Go’s unique concurrency model.

Load Balancer vs. Reverse Proxy?

Load balancers and reverse proxies often overlap in functionality but aren’t mutually exclusive. While a load balancer focuses on distributing requests across multiple servers to ensure reliability and performance, a reverse proxy forwards requests from clients to back end child servers, sometimes acting as a load balancer in the process.

What I Built

// main.go

package main

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

func main() {
	// Initialize all servers as healthy for start-up
	for i := range HealthStatus {
		HealthStatus[i] = true
	}

	// Start health check as a go routine in the background
	go RunHealthCheck()

	// Set up the single reverse proxy with dynamic routing
	proxy := httputil.NewSingleHostReverseProxy(nil)
	// Reverse Proxy Struct requires a director function so 
    // I made my own custom Director called dynamicDirector
	proxy.Director = dynamicDirector

	// Runs the load balancer server
	http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
		proxy.ServeHTTP(res, req)
	})
	log.Fatal(http.ListenAndServe(":8000", nil))
}

// The Director function is a request transformer. 
// When a request comes to the reverse proxy, 
// Director modifies it so that the request looks as if it 
// was originally intended for the backend server. 
// After the Director function makes its modifications, 
// the proxy forwards the request to the appropriate target server.
func dynamicDirector(req *http.Request) {
	// targetURL := getNextServer()
	targetURL := GetNextHealthyServer()
	req.URL.Scheme = targetURL.Scheme
	req.URL.Host = targetURL.Host
	req.URL.Path = targetURL.Path
}
// healthChecker.go

package main

import (
	"github.com/go-co-op/gocron"
	"log"
	"net/http"
	"net/url"
	"sync"
	"time"
)

var (
	ServerURLs = []*url.URL{
		ParseURL("http://127.0.0.1:5000/"),
		ParseURL("http://127.0.0.1:5001/"),
		ParseURL("http://127.0.0.1:5002/"),
		ParseURL("http://127.0.0.1:5003/"),
		ParseURL("http://127.0.0.1:5004/"),
	}
	HealthStatus = make([]bool, len(ServerURLs))
	Mu           sync.RWMutex
)

// Functions to perform health checks on servers

// Parses a string URL and returns a pointer to a url object which is required by rev proxy's Director func
func ParseURL(urlStr string) *url.URL {
	u, _ := url.Parse(urlStr)
	return u
}

// Go routine to run health checks on a separate thread
func RunHealthCheck() {
	// Utilizes cron jobs to run checkServerHealth every 2 seconds
	// Here we create a Cron Scheduler Object from the gocron library
	s := gocron.NewScheduler(time.Local)

	// We loop through every server URL and check each server's health
	for idx, server := range ServerURLs {
		// We want the scheduler to perform a given function, every 2 seconds
		// The Do method takes a function as an argument which is where we will put checkServerHealth
		s.Every(2).Second().Do(func() {
			// We run the function and then log the server's health
			CheckServerHealth(idx, server)
			if HealthStatus[idx] {
				log.Printf("'%s' is healthy!", server)
			} else {
				log.Printf("'%s' is not healthy!", server)
			}
		})
	}
	// cmd to start the func, use "<-" syntax if the function has a return value
	s.StartAsync()
}

// Pings each server to assess health status, updating shared state accordingly
func CheckServerHealth(index int, server *url.URL) {
	// We check server health by pinging the server and checking the response
	resp, err := http.Get(server.String())

	// Acquire a write lock to access HealthStatus atomically
	Mu.Lock()
	// Deferring delays the unlock until the function returns
	defer Mu.Unlock()

	// If we don't get a 200, it means the server is offline, set the health status to false
	if err != nil || resp.StatusCode != http.StatusOK {
		HealthStatus[index] = false
		// If we do get a 200, it means the server is online, set the health status to true
	} else {
		HealthStatus[index] = true
	}
	if resp != nil {
		resp.Body.Close()
	}
}

func GetNextHealthyServer() *url.URL {
	// Acquire a read lock to access HealthStatus atomically
	Mu.RLock()
	// Deferring delays the unlock until the function returns
	defer Mu.RUnlock()

	// Loop to find the next healthy server
	for i := 0; i < len(ServerURLs); i++ {
		if HealthStatus[i] {
			return ServerURLs[i]
		}
	}
	// Fallback: if all servers are unhealthy, return a default server or error
	log.Printf("Error")
	return nil
}
# server.py

import sys
from flask import Flask
app = Flask(__name__)

serverName = sys.argv[1]

@app.route('/')
def hello():
    return "Hello from " + serverName + " !"

if __name__ == '__main__':
    app.run(port=int(sys.argv[2]))

Why Should I Care?

These technologies are embedded in everything we use:

  • Streaming services: YouTube, Netflix, Spotify
  • E-commerce platforms: Amazon, eBay, Shopify
  • Social Media: Instagram, Twitter, Facebook
  • Online Banking: Chase, Venmo, PayPal
  • Gaming: Steam, Epic Games, Riot Games
  • Educational Platforms: Coursera, Zoom, Teams
  • News Platforms: New York Times, BBC, CNN
  • APIs and Microservices: Stripe, Twilio

It may seem like obscure tech jargon, but these technologies make a big difference in our lives. Some real world analogies of these include: restaurant waiter, traffic controller, call centers, tour guides, teachers, just to name a few.

Reverse proxies and load balancers keep things organized, responsive, and efficient—whether it’s directing traffic, managing workloads, or guiding users to the right resources. These are essential roles that ensure systems, just like real-world environments, can handle demand and traffic smoothly without slowing down or crashing.

We rely on load balancing and reverse proxy technology more often than we realize — whether it’s streaming a video, shopping online, or even checking our bank balance. They keep our online interactions smooth, efficient, and fast.

By learning about these technologies, we can potentially improve the systems around us and give us insights we did not have before. It is an important paradigm that can be applied to improve upon many different systems.

What I Learned

I had a lot of fun experimenting and learning how to build servers in Go. I really enjoyed learning about how to handle concurrent requests using mutexes and Goroutines. Coming from JavaScript, I was surprised by Go’s simplicity and efficiency. Learning to handle concurrent requests with Goroutines and mutexes was a rewarding experience, and Go's documentation was a fantastic guide throughout. Picking up the language was not extremely difficult either which I can attribute to its hybrid nature: combining the performance of C with the syntactic simplicity of Python.

Recap

In this article, I explored the concepts of load balancing and reverse proxies by building a simple system in Go. Load balancers distribute incoming requests across multiple servers to improve reliability and performance, while reverse proxies forward client requests to back end servers. I combined both technologies into a single system that not only routes requests but also ensures the health of back end servers through periodic checks.

Through the use of Goroutines and mutexes, I was able to dynamically route requests to child servers and perform periodic health checks on them. I also discussed the real-world importance of these technologies in systems we use every day such as YouTube, Netflix, and Amazon, highlighting their role in maintaining smooth, responsive services.

By diving into these technologies with this project, I gained valuable hands-on experience with Go and built a deeper understanding of critical systems.

© 2025 Huy Phan | All Rights Reserved