How TeslaMate's HTTP Client Implements Circuit Breakers Using the Erlang Fuse Library
TeslaMate wraps its Tesla API HTTP client in circuit breakers using the Erlang :fuse library to halt requests after consecutive failures, isolating the global API service and individual vehicle processes from cascading errors.
TeslaMate is an open-source, self-hosted data logger for Tesla vehicles that requires robust resilience against Tesla API rate limits and intermittent outages. By implementing circuit breaker logic in lib/teslamate/api.ex and lib/teslamate/vehicles/vehicle.ex, the application automatically detects fault conditions and stops sending requests until the service recovers.
Installing Global and Per-Vehicle Circuit Breakers
The circuit breaker implementation relies on named fuses created during process initialization. Each fuse uses a standard strategy that defines a failure threshold and a time window, paired with a reset strategy that determines how long the circuit stays open.
Global API Client Fuse
When the TeslaMate.Api GenServer starts, it installs a fuse keyed to the process name that protects all unauthorized error scenarios:
# lib/teslamate/api.ex – init/1 (lines 101-105)
:fuse.install(
fuse_name(name),
{{:standard, 5, :timer.minutes(10)}, {:reset, :timer.hours(9999)}}
)
fuse_name/1generates an atom like:"#{name}.unauthorized"(e.g.,:tesla_api.unauthorized).- The policy
{:standard, 5, :timer.minutes(10)}melts the fuse after 5 failures within 10 minutes. - The reset strategy
{:reset, :timer.hours(9999)}keeps the circuit open for 9,999 hours after it blows, effectively requiring manual intervention or a successful reset to close.
Per-Vehicle Fuses for Granular Protection
Each vehicle process in TeslaMate.Vehicles.Vehicle installs two dedicated fuses during its init/1 callback:
# lib/teslamate/vehicles/vehicle.ex – init/1 (lines 198-207)
fuses = [
{:vehicle_not_found, {{:standard, 8, :timer.minutes(20)}, {:reset, :timer.minutes(10)}}},
{:api_error, {{:standard, 3, :timer.minutes(10)}, {:reset, :timer.minutes(5)}}}
]
for {key, opts} <- fuses do
name = fuse_name(key, data.car.id)
:ok = :fuse.install(name, opts)
:ok = :fuse.circuit_enable(name)
end
api_errorblows after 3 failures in 10 minutes and resets after 5 minutes.vehicle_not_foundtolerates 8 failures over 20 minutes before blowing, reflecting its lower severity, and resets after 10 minutes.:fuse.circuit_enable/1activates the fuse so it begins monitoring failures immediately.
Tripping the Circuit on Authentication Failures
When an API call returns an unauthorized error, the client melts the fuse to record the failure, then checks the fuse state to decide whether to retry or abort.
Melting the Fuse in handle_result
The handle_result/3 function in lib/teslamate/api.ex processes the Tesla API response. On an unauthorized error, it melts the fuse and interrogates the circuit state:
# lib/teslamate/api.ex – handle_result/3 (lines 72-84)
{:error, %TeslaApi.Error{reason: :unauthorized}} ->
:ok = :fuse.melt(fuse_name(name))
case :fuse.ask(fuse_name(name), :sync) do
:blown ->
true = :ets.delete(name, :auth)
{:error, :not_signed_in}
:ok ->
send(name, :refresh_auth)
{:error, :unauthorized}
end
:fuse.melt/1increments the failure counter for the named circuit.:fuse.ask/2with the:syncoption returns:blownif the threshold was exceeded, or:okif the circuit is still closed.- If the fuse is blown, the client deletes the cached authentication from ETS and returns
{:error, :not_signed_in}, forcing a fresh sign-in. - If the circuit remains closed, it triggers an asynchronous token refresh via
send(name, :refresh_auth).
Querying Circuit State Before Requests
Before performing health checks or state-changing operations, vehicle processes validate that their specific fuses are intact.
Checking Health with healthy?/1
The healthy?/1 function queries both the api_error and vehicle_not_found fuses to determine if the vehicle process should attempt API calls:
# lib/teslamate/vehicles/vehicle.ex – healthy?/1 (lines 49-55)
def healthy?(car_id) do
with :ok <- :fuse.ask(fuse_name(:api_error, car_id), :sync),
:ok <- :fuse.ask(fuse_name(:vehicle_not_found, car_id), :sync) do
true
else
:blown -> false
end
end
If either fuse returns :blown, the function returns false, and the vehicle process skips polling until the reset interval expires.
Resetting Fuses After Successful Recovery
When the API client successfully authenticates or completes a request after failures, it clears the fuse state to restore normal operation.
Resetting the Global Fuse
After a successful sign-in, the global unauthorized fuse is explicitly reset:
# lib/teslamate/api.ex – after successful sign-in (line 153)
:ok = :fuse.reset(fuse_name(state.name))
:fuse.reset/1 immediately closes the circuit and clears the failure counter, allowing subsequent requests to proceed without waiting for the automatic reset timer.
Practical Code Examples
Starting the API Client with Circuit Breaker Protection
When you start the API client, the fuse installs automatically with a 5-failure threshold:
{:ok, pid} = TeslaMate.Api.start_link(name: :tesla_api)
# Fuse :tesla_api.unauthorized is now active
Checking Vehicle Health Before Polling
Verify a vehicle's circuit state before making API requests:
car_id = 42
if TeslaMate.Vehicles.Vehicle.healthy?(car_id) do
{:ok, state} = TeslaMate.Vehicles.Vehicle.get_state(car_id)
else
IO.puts("Circuit breaker open for vehicle #{car_id} – skipping request")
end
Manually Resetting a Blown Fuse
After manually refreshing a token or resolving an API issue, reset the fuse to resume operations immediately:
:fuse.reset(:tesla_api.unauthorized)
Summary
- TeslaMate.Api installs a global fuse in
lib/teslamate/api.exthat blows after 5 unauthorized errors in 10 minutes. - TeslaMate.Vehicles.Vehicle creates per-vehicle fuses for
api_error(3 failures) andvehicle_not_found(8 failures) to isolate individual vehicle issues. :fuse.melt/1records failures, while:fuse.ask/2checks if the circuit is:blownor:okbefore proceeding.:fuse.reset/1manually clears the failure state after successful recovery, whereas automatic reset occurs after the configured timeout.
Frequently Asked Questions
What triggers the circuit breaker to open in TeslaMate?
The circuit breaker opens when :fuse.melt/1 records enough consecutive failures to exceed the threshold defined in the fuse policy. For the global API client, this occurs after 5 unauthorized errors within 10 minutes. For individual vehicles, the api_error fuse blows after 3 failures in 10 minutes.
How does TeslaMate recover after a circuit breaker trips?
Recovery happens automatically after the reset timer expires (5 minutes for api_error, 10 minutes for vehicle_not_found, or 9,999 hours for the global unauthorized fuse). Alternatively, calling :fuse.reset/1 manually after a successful authentication immediately closes the circuit and clears the failure count.
Why does TeslaMate use per-vehicle fuses instead of only a global fuse?
Per-vehicle fuses in lib/teslamate/vehicles/vehicle.ex provide finer-grained fault isolation. If one vehicle consistently returns "not found" or API errors, it can be isolated without blocking requests for other vehicles sharing the same Tesla account. This prevents a single problematic vehicle from degrading the entire fleet's data syncing.
What is the difference between :fuse.melt and :fuse.reset?
:fuse.melt/1 increments the failure counter and potentially blows the circuit if the threshold is exceeded. :fuse.reset/1 clears all failure history and immediately closes an open circuit, typically called after a successful request or manual recovery to restore service before the automatic reset timer expires.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →