Week 2 Mini Project: Concurrent URL Health Checker
Build a production-quality health checking tool used by your load balancer (Week 7) to detect dead backends.
Full Implementation
// healthcheck/main.go
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"sync"
"time"
)
type CheckResult struct {
URL string `json:"url"`
Status int `json:"status,omitempty"`
Latency time.Duration `json:"latency_ms"`
Healthy bool `json:"healthy"`
Error string `json:"error,omitempty"`
CheckedAt time.Time `json:"checked_at"`
}
type Config struct {
Concurrency int
Timeout time.Duration
TotalBudget time.Duration
OutputFile string
}
func checkURL(ctx context.Context, url string, timeout time.Duration) CheckResult {
result := CheckResult{
URL: url,
CheckedAt: time.Now(),
}
reqCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
start := time.Now()
req, err := http.NewRequestWithContext(reqCtx, "GET", url, nil)
if err != nil {
result.Error = err.Error()
result.Latency = time.Since(start)
return result
}
resp, err := http.DefaultClient.Do(req)
result.Latency = time.Since(start)
if err != nil {
result.Error = err.Error()
return result
}
defer resp.Body.Close()
result.Status = resp.StatusCode
result.Healthy = resp.StatusCode >= 200 && resp.StatusCode < 400
return result
}
func run(cfg Config, urls []string) []CheckResult {
ctx, cancel := context.WithTimeout(context.Background(), cfg.TotalBudget)
defer cancel()
jobs := make(chan string, len(urls))
results := make(chan CheckResult, len(urls))
var wg sync.WaitGroup
for i := 0; i < cfg.Concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case url, ok := <-jobs:
if !ok { return }
results <- checkURL(ctx, url, cfg.Timeout)
case <-ctx.Done():
return
}
}
}()
}
for _, url := range urls {
jobs <- url
}
close(jobs)
go func() {
wg.Wait()
close(results)
}()
var out []CheckResult
for r := range results {
out = append(out, r)
}
return out
}
func main() {
cfg := Config{
Concurrency: 10,
Timeout: 5 * time.Second,
TotalBudget: 30 * time.Second,
OutputFile: "results.json",
}
// Read URLs from stdin or args
var urls []string
if len(os.Args) > 1 {
urls = os.Args[1:]
} else {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
if url := scanner.Text(); url != "" {
urls = append(urls, url)
}
}
}
if len(urls) == 0 {
// Demo mode
urls = []string{
"https://example.com",
"https://golang.org",
"https://httpbin.org/status/200",
"https://httpbin.org/status/500",
}
}
results := run(cfg, urls)
// Write JSON output
f, err := os.Create(cfg.OutputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "output error: %v\n", err)
os.Exit(1)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
enc.Encode(results)
// Print summary
var healthy, unhealthy int
for _, r := range results {
if r.Healthy { healthy++ } else { unhealthy++ }
status := "OK "
if !r.Healthy { status = "ERR" }
fmt.Printf("[%s] %s — %dms\n", status, r.URL, r.Latency.Milliseconds())
}
fmt.Printf("\n%d healthy, %d unhealthy (results in %s)\n", healthy, unhealthy, cfg.OutputFile)
}
Run It
go run . https://example.com https://golang.org https://httpbin.org/status/500
# Or from a file
cat urls.txt | go run .
Engineer Takeaway
This pattern (worker pool + channel pipeline + context cancellation) is in every batch job, health checker, and data pipeline. Your load balancer in Week 7 runs exactly this — 5 workers, each probing a backend every 5 seconds, with context timeout to prevent hanging checks from blocking the health state machine.