Frontend Labs
⋯

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.

Generated from week2.md · 8 Oct 2026 16:36