Skip to Content
Course content

229: Circuit Breaker Pattern in Go

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

I remember a Friday afternoon a few years back when I was on call for a logistics platform. One of our third-party address validation APIs started lagging—not crashing, but taking nearly 29 seconds to respond. Our Go services had a 30-second timeout. Suddenly, every single request to our checkout endpoint was hanging. Our goroutine count spiked from a few hundred to over 15,000 in minutes because each request was just sitting there, waiting. We didn't just lose address validation; we took down the entire checkout process. We were essentially DDoS-ing ourselves because we were too polite to stop calling a service that was clearly drowning.

That's where the Circuit Breaker pattern comes in. In a distributed system, you have to assume that things will fail. The goal isn't just to handle the error, but to prevent a failing dependency from dragging your entire application into the abyss. It works exactly like an electrical circuit breaker in your house: when it detects a fault (too many errors), it "trips," cutting off the flow of requests to the failing service to give it space to recover and to protect your own resources.

Managing the Three States of Failure

To implement this in Go, you need to track the state of the connection to the external service. I usually think of it as a state machine with three distinct phases: Closed, Open, and Half-Open.

  • Closed: Everything is normal. Requests flow through to the dependency. We keep a count of recent failures, but as long as they stay below a certain threshold, we stay Closed.
  • Open: The threshold was hit. The circuit "trips." Now, any request that attempts to call the dependency fails immediately without even trying to hit the network. This is the "fail-fast" mechanism that saves your goroutines from piling up.
  • Half-Open: After a "sleep window" (say, 60 seconds), we move to Half-Open. We allow a single "probe" request through. If it succeeds, we assume the service is healthy and flip back to Closed. If it fails, we immediately go back to Open and reset the timer.

Implementing a Breaker in Go

While you can write your own state machine, the Go community generally leans on proven libraries like sony/gobreaker because handling the concurrency of state transitions (especially moving from Open to Half-Open) can be tricky. Here is how you'd typically wrap a flaky API call using a circuit breaker.

package main

import (
	"fmt"
	"time"
	"github.com/sony/gobreaker"
)

func main() {
	// Configure the breaker
	settings := gobreaker.Settings{
		Name:        "HTTP GET",
		MaxRequests: 3,                // Max requests allowed through when Half-Open
		Interval:    5 * time.Second,  // Clear counts every 5 seconds when Closed
		Timeout:     10 * time.Second, // How long to stay Open before switching to Half-Open
		ReadyToTrip: func(counts gobreaker.Counts) bool {
			// Trip if more than 3 requests failed
			return counts.ConsecutiveFailures > 3
		},
	}

	cb := gobreaker.NewCircuitBreaker(settings)

	// We wrap our flaky call inside the Execute method
	for i := 0; i < 10; i++ {
		result, err := cb.Execute(func() (interface{}, error) {
			return callFlakyAPI()
		})

		if err != nil {
			fmt.Printf("Request %d: Error: %v\n", i, err)
		} else {
			fmt.Printf("Request %d: Success: %v\n", i, result)
		}
		time.Sleep(1 * time.Second)
	}
}

func callFlakyAPI() (interface{}, error) {
	// Simulating a failure
	return nil, fmt.Errorf("service unavailable")
}

Notice that the Execute method handles the state transitions for you. If the circuit is Open, Execute returns an error immediately without even running the inner function. This is the magic that prevents your service from hanging. I've found that the ReadyToTrip logic is the most important part to tune; sometimes you want it to trip based on a percentage of failures rather than consecutive ones, depending on how volatile the dependency is.

When to Use a Breaker vs. a Timeout

I often see developers confuse timeouts with circuit breakers. A timeout is your last line of defense for a single request—it stops a request from waiting forever. A circuit breaker is a strategic defense for your entire system—it stops you from making requests you already know are likely to fail.

If you only use timeouts, you're still hitting the failing server thousands of times per second, which might actually prevent that server from ever recovering. By using a circuit breaker, you're giving the downstream service breathing room. It's the difference between shouting at someone who is having a panic attack and giving them five minutes of silence to calm down before trying to talk again.




📋 Practical Task

Exercise: Implementing a Resilient Payment Gateway Wrapper

You are integrating a third-party payment processor that is known to be unstable. Your goal is to create a wrapper that prevents your application from hanging when the processor goes down.

Requirements:

  • Create a mock function ProcessPayment() that randomly returns an error 70% of the time to simulate instability.
  • Implement a circuit breaker (using sony/gobreaker or a custom state machine) with the following rules:
    • The circuit should trip (Open) after 3 consecutive failures.
    • The circuit should remain Open for 5 seconds before transitioning to Half-Open.
    • In the Half-Open state, only 1 request should be allowed through to test the service.
  • Write a main function that attempts to process 20 payments in a loop with a small delay (e.g., 500ms) between them.
  • Print the result of each attempt, clearly indicating if the failure was caused by the ProcessPayment function itself or if the Circuit Breaker blocked the request (Fail-Fast).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.