This guide explains how to integrate Tripswitch into a production service using an official SDK. It focuses on where Tripswitch fits in your code and what responsibilities remain yours. Examples use Go, but the integration pattern applies across all Tripswitch SDKs (Go, Python, TypeScript).
The Integration Loop
Every interaction with a circuit breaker follows four steps:
Check state — Ask whether the breaker is Open, Closed, or Half-Open.
Decide — Based on that state, choose whether to proceed with the call.
Execute — If proceeding, call the dependency.
Report outcome — Tell Tripswitch whether the call succeeded or failed.
These steps separate cleanly between the SDK and your service:
The SDK handles state lookup and outcome reporting.
Your service handles the decision and the execution.
This separation matters. Breaker state lives on the Tripswitch server and is synchronized to your service via the SDK. But the SDK never calls your dependencies for you. You remain in control of what happens when a breaker is Open, how you handle failures, and when to retry.
What the SDK Guarantees vs. What the Service Owns
SDK Responsibilities
Responsibility
What it means
Efficient state lookup
State checks are local (no network round-trip per call). The SDK maintains a local cache synchronized from the server.
Safe defaults
Unknown breakers fail open. Lost server connectivity doesn’t block your traffic. (See below.)
Consistent request shaping
Half-Open throttling uses the same probabilistic model across all SDK instances.
Outcome batching
Samples are buffered and sent efficiently. Your hot path isn’t blocked by telemetry.
Clear key distinction
Project keys are for runtime operations. Admin keys are for management. These never mix.
Service Responsibilities
Responsibility
What it means
Execution control
You call the dependency. The SDK provides state, not execution.
Retry logic
The SDK doesn’t retry. If you want retries, you implement them.
Fallback behavior
When a breaker is Open, the SDK returns an error. What you do with that error is up to you.
Concurrency limits
The SDK doesn’t limit how many requests you send. If you need concurrency control, implement it yourself.
Degradation strategy
Cached responses, reduced functionality, error pages — these are your decisions.
Fail-open by default
Tripswitch defaults to fail-open on uncertainty: when state is unknown or stale, traffic proceeds. This avoids cascading failures caused by control-plane outages. If this is unacceptable for your system, configure fail-open to false explicitly.
Canonical Example
This example shows the full integration loop: checking state, executing a dependency call, and reporting the outcome.
packagemainimport("context""log""net/http""time""github.com/tripswitch-dev/tripswitch-go")funcmain(){// Initialize the client with project credentials.
// NewClient blocks until the initial breaker state is synced via SSE.
ctx,cancel:=context.WithTimeout(context.Background(),10*time.Second)defercancel()ts,err:=tripswitch.NewClient(ctx,"proj_abc123",tripswitch.WithAPIKey("eb_pk_live_..."),tripswitch.WithIngestSecret("your-64-char-hex-secret"),)iferr!=nil{log.Fatalf("Failed to initialize Tripswitch: %v",err)}deferts.Close(context.Background())// Execute a protected call
resp,err:=tripswitch.Execute(ts,context.Background(),func()(*http.Response,error){returnhttp.Get("https://payments.example.com/charge")},// Gate on this breaker's state
tripswitch.WithBreakers("payment-gateway"),// Route samples to this router
tripswitch.WithRouter("rtr_uuid_from_dashboard"),// Auto-report latency
tripswitch.WithMetrics(map[string]any{"latency":tripswitch.Latency,}),)// Handle the result
iferr!=nil{iftripswitch.IsBreakerError(err){// Breaker is open — the call was never attempted
log.Println("Payment gateway unavailable, using fallback")return}// The call was attempted but failed
log.Printf("Payment failed: %v",err)return}// Success
deferresp.Body.Close()log.Printf("Payment succeeded: %d",resp.StatusCode)}
importloggingimporthttpximporttripswitchlogging.basicConfig(level=logging.INFO)log=logging.getLogger(__name__)# Initialize the client with project credentials.
# The context manager blocks until the initial breaker state is synced via SSE.
withtripswitch.Client("proj_abc123",api_key="eb_pk_live_...",ingest_secret="your-64-char-hex-secret",timeout=10.0,)asts:# Execute a protected call
try:resp=ts.execute(lambda:httpx.get("https://payments.example.com/charge"),# Gate on this breaker's state
breakers=["payment-gateway"],# Route samples to this router
router="rtr_uuid_from_dashboard",# Auto-report latency
metrics={"latency":tripswitch.Latency},)excepttripswitch.BreakerOpenError:# Breaker is open — the call was never attempted
log.info("Payment gateway unavailable, using fallback")excepthttpx.HTTPErrorasexc:# The call was attempted but failed
log.error("Payment failed: %s",exc)else:# Success
log.info("Payment succeeded: %d",resp.status_code)
import{Client,Latency,BreakerOpenError}from"@tripswitch-dev/tripswitch";asyncfunctionmain(){// Initialize the client with project credentials.// Client.create() blocks until the initial breaker state is synced via SSE.constts=awaitClient.create({projectId:"proj_abc123",apiKey:"eb_pk_live_...",ingestSecret:"your-64-char-hex-secret",timeout:10_000,});try{// Execute a protected callconstresp=awaitts.execute(()=>fetch("https://payments.example.com/charge"),{// Gate on this breaker's statebreakers:["payment-gateway"],// Route samples to this routerrouter:"rtr_uuid_from_dashboard",// Auto-report latencymetrics:{latency:Latency},},);// Successconsole.log(`Payment succeeded: ${resp.status}`);}catch(err){if(errinstanceofBreakerOpenError){// Breaker is open — the call was never attemptedconsole.warn("Payment gateway unavailable, using fallback");return;}// The call was attempted but failedconsole.error("Payment failed:",err);}finally{awaitts.close();}}main();
# In your supervision tree (application.ex)children=[{Tripswitch.Client,project_id:"proj_abc123",api_key:"eb_pk_live_...",ingest_secret:"your-64-char-hex-secret",name:MyApp.Tripswitch}]# Execute a protected callresult=Tripswitch.execute(MyApp.Tripswitch,fn->HTTPClient.get("https://payments.example.com/charge")end,# Gate on this breaker's statebreakers:["payment-gateway"],# Route samples to this routerrouter:"rtr_uuid_from_dashboard",# Auto-report latencymetrics:%{"latency"=>:latency})# Handle the resultcaseresultdo{:error,:breaker_open}-># Breaker is open — the call was never attemptedLogger.warning("Payment gateway unavailable, using fallback"){:error,reason}-># The call was attempted but failedLogger.error("Payment failed: #{inspect(reason)}"){:ok,resp}->Logger.info("Payment succeeded: #{resp.status}")end
importdev.tripswitch.*;importjava.net.URI;importjava.net.http.*;importjava.time.Duration;importjava.util.Map;// Create client (blocks until SSE state sync completes)
try(TripSwitch ts =TripSwitch.builder("proj_abc123").apiKey("eb_pk_live_...").ingestSecret("ik_...").build(Duration.ofSeconds(10))){var httpClient =HttpClient.newHttpClient();var request =HttpRequest.newBuilder().uri(URI.create("https://payments.example.com/charge")).build();try{// Execute a protected call
String body = ts.execute(()-> httpClient.send(request,HttpResponse.BodyHandlers.ofString()).body(),// Gate on this breaker's state
TripSwitch.withBreakers("payment-gateway"),// Route samples to this router
TripSwitch.withRouter("rtr_uuid_from_dashboard"),// Auto-report latency
TripSwitch.withMetrics(Map.of("latency",TripSwitch.LATENCY)));// Success
System.out.println("Payment succeeded");}catch(BreakerOpenExceptione){// Breaker is open — the call was never attempted
System.out.println("Payment gateway unavailable, using fallback");}catch(Exceptione){// The call was attempted but failed
System.out.println("Payment failed: "+ e.getMessage());}}
usetripswitch::{Client, ExecuteOptions, MetricValue};usestd::time::Duration;#[tokio::main]async fnmain()->Result<(), Box<dyn std::error::Error>>{// Create client (blocks until SSE state sync completes)
let client =Client::builder("proj_abc123").api_key("eb_pk_live_...").ingest_secret("ik_...").init_timeout(Duration::from_secs(10)).build().await?;// Execute a protected call
let result = client
.execute(||async {reqwest::get("https://payments.example.com/charge").await?.text().await
},ExecuteOptions::new()// Gate on this breaker's state
.breakers(&["payment-gateway"])// Route samples to this router
.router("rtr_uuid_from_dashboard")// Auto-report latency
.metric("latency",MetricValue::Latency),).await;// Handle the result
match result {Ok(body)=>println!("Payment succeeded: {}", body),Err(e)if e.is_breaker_error()=>{// Breaker is open — the call was never attempted
println!("Payment gateway unavailable, using fallback");}Err(e)=>println!("Payment failed: {e}"),} client.close().await;Ok(())}
importqualifiedData.Map.StrictasMapimportNetwork.HTTP.Simple(httpBS,parseRequest)importTripswitchmain::IO()main =dolet cfg = defaultConfig
{ cfgProjectID ="proj_abc123", cfgApiKey ="eb_pk_live_...", cfgIngestSecret ="ik_..." }
-- withClient blocks until SSE state sync completes
withClient cfg $\client ->do req <- parseRequest "https://payments.example.com/charge"-- Execute a protected call
result <- execute client defaultExecConfig
{ -- Gate on this breaker's state
ecBreakers = ["payment-gateway"]
-- Route samples to this router
, ecRouterID ="rtr_uuid_from_dashboard"-- Auto-report latency
, ecMetrics =Map.singleton "latency"MetricLatency }
(httpBS req)
-- Handle the result
case result ofLeftErrBreakerOpen->-- Breaker is open — the call was never attempted
putStrLn "Payment gateway unavailable, using fallback"Left err ->-- The call was attempted but failed
print err
Right _resp -> putStrLn "Payment succeeded"
What happens in each state
State
Behavior
Closed
The task executes. The outcome is reported.
Open
The task does not execute. execute returns a breaker-open error immediately.
Half-Open
Some calls are allowed through (based on the configured allow rate). Rejected calls return a breaker-open error.
The SDK’s execute function handles this loop for you, but the pattern remains explicit: check, decide, execute, report. execute is a convenience, not a requirement. Advanced integrations may perform explicit state checks and report outcomes manually, but the underlying pattern remains the same.
Half-Open Behavior
Half-Open is the most dangerous state to get wrong. Understanding it prevents cascading failures during recovery.
Why Half-Open exists
When a breaker trips Open, it stops all traffic to a failing dependency. But eventually you need to test whether the dependency has recovered. Half-Open allows a controlled number of probe requests through.
How the SDK handles Half-Open
In Half-Open state, the server specifies an allow rate (e.g., 0.1 means 10% of requests). The SDK applies this probabilistically:
Each call has a random chance of being allowed through.
Rejected calls return a breaker-open error without executing.
Allowed calls execute and report their outcome.
This happens automatically inside Execute. You don’t need to implement probe logic yourself.
What you must still enforce
The SDK handles per-request throttling, but it doesn’t know your system’s broader context.
This surprises teams: Allow rates apply per instance, not globally. A 10% allow rate across 50 instances means 50 concurrent probe requests — not “gentle” recovery traffic.
Your responsibility
Why
Global probe limits
If you have 100 instances each allowing 10% through, you’re sending 10 requests concurrently. Consider whether your recovering dependency can handle that.
Timeout enforcement
Probe requests should have tight timeouts. A slow probe ties up resources and delays recovery detection.
Outcome accuracy
A successful HTTP 200 with bad data is still a success to the breaker unless you tell it otherwise. Use a custom error evaluator if you need custom failure detection.
Example: Custom failure detection for probes
resp,err:=tripswitch.Execute(ts,ctx,callPaymentAPI,tripswitch.WithBreakers("payment-gateway"),tripswitch.WithRouter("rtr_uuid"),tripswitch.WithErrorEvaluator(func(errerror)bool{// Don't count client errors as breaker failures
varhttpErr*HTTPErroriferrors.As(err,&httpErr){returnhttpErr.StatusCode>=500}returnerr!=nil}),)
resp=ts.execute(call_payment_api,breakers=["payment-gateway"],router="rtr_uuid",error_evaluator=lambdae:(# Don't count client errors as breaker failures
e.status_code>=500ifisinstance(e,HTTPError)elseTrue),)
constresp=awaitts.execute(callPaymentAPI,{breakers:["payment-gateway"],errorEvaluator:(err)=>{// Don't count client errors as breaker failuresif(errinstanceofHTTPError){returnerr.statusCode>=500;}returntrue;},});
Tripswitch.execute(MyApp.Tripswitch,fn->call_payment_api()end,breakers:["payment-gateway"],router:"rtr_uuid",error_evaluator:fn# Don't count client errors as breaker failures{:error,%HTTPError{status:status}}->status>=500{:error,_}->true_->falseend)
String body = ts.execute(()->callPaymentAPI(),TripSwitch.withBreakers("payment-gateway"),TripSwitch.withRouter("rtr_uuid"),TripSwitch.withErrorEvaluator(e->{// Don't count client errors as breaker failures
if(e instanceofHttpResponseException hre){return hre.getStatusCode()>=500;}returntrue;// non-HTTP errors are failures
}));
let result = client
.execute(||async {call_payment_api().await },ExecuteOptions::new().breakers(&["payment-gateway"]).router("rtr_uuid").error_evaluator(|err|{// Don't count client errors as breaker failures
!err.to_string().contains("status: 4")}),).await;
result <- execute client defaultExecConfig
{ ecBreakers = ["payment-gateway"]
, ecRouterID ="rtr_uuid", ecErrorEvaluator =Just$\ex ->-- Don't count client errors as breaker failures
case fromException ex ofJust (HttpError status _) -> status >=500Nothing->True-- non-HTTP errors are failures
}
callPaymentAPI
Half-Open is not magic. The SDK gives you safe defaults, but you remain responsible for ensuring probe traffic doesn’t overwhelm a recovering service.
Polling vs. Subscription
The SDK needs to know the current breaker state. There are two approaches: polling the server periodically, or subscribing to a stream of state changes.
The SDK uses subscription (SSE)
The Go SDK maintains a persistent Server-Sent Events connection to Tripswitch. When breaker state changes, the server pushes an update immediately.
Aspect
Subscription (SSE)
Latency
State changes arrive in milliseconds.
Efficiency
No repeated requests. One connection per client.
Failure mode
If the connection drops, state becomes stale.
If you build your own client or operate in restricted environments where persistent connections aren’t viable, polling remains a valid fallback.
What happens when the connection fails
The SDK reconnects automatically with exponential backoff. During disconnection:
Default behavior (fail-open): Unknown or stale state allows traffic through. Your dependency handles the load.
Fail-closed option: If you configure fail-open to false, lost connectivity blocks all traffic until reconnection.
Fail-open is the default because blocking all traffic due to a Tripswitch outage is usually worse than allowing some potentially-failing requests through.
Checking connection health
stats:=ts.Stats()if!stats.SSEConnected{log.Println("WARNING: Tripswitch state sync disconnected, operating on stale data")}
stats=ts.stats()ifnotstats.sse_connected:logger.warning("Tripswitch state sync disconnected, operating on stale data")
const{sseConnected}=ts.stats;if(!sseConnected){console.warn("Tripswitch state sync disconnected, operating on stale data");}
stats=Tripswitch.stats(MyApp.Tripswitch)unlessstats.sse_connecteddoLogger.warning("Tripswitch state sync disconnected, operating on stale data")end
SDKStats stats = ts.stats();if(!stats.sseConnected()){ log.warn("Tripswitch state sync disconnected, operating on stale data");}
let stats = client.stats();if!stats.sse_connected {log::warn!("Tripswitch state sync disconnected, operating on stale data");}
stats <- getStats client
unless (sseConnected stats) $ putStrLn "WARNING: Tripswitch state sync disconnected, operating on stale data"
Common Mistakes
These patterns cause problems in production. Each is covered in detail in the full Common Mistakes guide.
Ignoring Open state
// Wrong: Calling the dependency anyway
resp,err:=tripswitch.Execute(ts,ctx,callDependency,tripswitch.WithBreakers("my-breaker"),)iferr!=nil{resp,err=callDependency()// Defeats the purpose
}
# Wrong: Calling the dependency anyway
try:resp=ts.execute(call_dependency,breakers=["my-breaker"])excepttripswitch.BreakerOpenError:resp=call_dependency()# Defeats the purpose
// Wrong: Calling the dependency anywaytry{constresp=awaitts.execute(callDependency,{breakers:["my-service"]});}catch(err){constresp=awaitcallDependency();// Defeats the purpose}
# Wrong: Calling the dependency anywaycaseTripswitch.execute(MyApp.Tripswitch,fn->call_dependency()end,breakers:["my-breaker"])do{:error,:breaker_open}->call_dependency()# Defeats the purposeresult->resultend
// Wrong: Calling the dependency anyway
try{ result = ts.execute(()->callDependency(),TripSwitch.withBreakers("my-breaker"));}catch(BreakerOpenExceptione){ result =callDependency();// Defeats the purpose
}
// Wrong: Calling the dependency anyway
let result = client
.execute(||async {call_dependency().await },ExecuteOptions::new().breakers(&["my-breaker"]),).await;if result.is_err(){let _result =call_dependency().await;// Defeats the purpose
}
-- Wrong: Calling the dependency anyway
result <- execute client defaultExecConfig
{ ecBreakers = ["my-breaker"] }
callDependency
case result ofLeftErrBreakerOpen-> callDependency -- Defeats the purpose
Left err -> throwIO err
Right val -> pure val
When the breaker is Open, it’s Open for a reason. Calling the dependency anyway undermines circuit breaker protection.
Flooding during Half-Open
If every instance allows 10% through and you have 50 instances, you’re sending 5x more probe traffic than intended. Coordinate probe limits at the system level, not just the SDK level.
Reporting outcomes inconsistently
// Wrong: Success reported even when response is unusable
resp,_:=tripswitch.Execute(ts,ctx,func()(*Response,error){r,err:=callAPI()iferr!=nil{returnnil,err}returnr,nil// Reported as success even if r.Status == 500
},tripswitch.WithBreakers("my-breaker"),tripswitch.WithRouter("rtr_uuid"),)
# Wrong: Success reported even when response is unusable
defcall():r=call_api()returnr# Reported as success even if r.status_code == 500
resp=ts.execute(call,breakers=["my-breaker"])
// Wrong: Success reported even when response is unusableconstresp=awaitts.execute(async()=>{constr=awaitcallAPI();returnr;// Reported as success even if r.status === 500},{breakers:["my-service"]},);
# Wrong: Success reported even when response is unusableTripswitch.execute(MyApp.Tripswitch,fn->{:ok,resp}=call_api(){:ok,resp}# Reported as success even if resp.status == 500end,breakers:["my-breaker"],router:"rtr_uuid")
// Wrong: Success reported even when response is unusable
try{HttpResponse<String> resp = ts.execute(()-> httpClient.send(request,HttpResponse.BodyHandlers.ofString()),TripSwitch.withBreakers("my-breaker"),TripSwitch.withRouter("rtr_uuid")// Missing: withErrorEvaluator to check resp.statusCode() >= 500
);// Reported as success even if resp.statusCode() == 500
}catch(BreakerOpenExceptione){/* ... */}
// Wrong: Success reported even when response is unusable
let result = client
.execute(||async {let r =call_api().await?;Ok::<_, reqwest::Error>(r)// Reported as success even if r.status() == 500
},ExecuteOptions::new().breakers(&["my-breaker"]).router("rtr_uuid"),// Missing: .error_evaluator to check response status
).await;
-- Wrong: Success reported even when response is unusable
result <- execute client defaultExecConfig
{ ecBreakers = ["my-breaker"]
, ecRouterID ="rtr_uuid"-- Missing: ecErrorEvaluator to check response status
}
(do r <- callAPI
pure r -- Reported as success even if responseStatus r == 500
)
The SDK reports based on whether your function returns an error. If a 500 response is a failure, return an error.
Treating Tripswitch like a wrapper
Tripswitch doesn’t wrap your dependencies. It doesn’t retry for you, doesn’t cache responses, and doesn’t transform errors. It provides breaker state and collects outcomes. Everything else is your code.
Summary
Integrating Tripswitch means understanding the division of responsibility:
Tripswitch tracks breaker state remotely and synchronizes it to your service.
The SDK provides efficient local state checks and batched outcome reporting.
Your service decides what to do with that state and executes all dependency calls.
The SDK makes the common case easy. But the safety guarantees come from understanding what the SDK does — and what it deliberately does not do.