DEV Community

Vin Lookup
Vin Lookup

Posted on

Circuit Breaker for NHTSA DecodeVinValues When Error Rates Spike

Retries with jitter help a single flaky request. They do not help when NHTSA vPIC is systematically unhealthy. If every decode retries three times, your free VIN tool multiplies traffic into an outage and turns a five-minute upstream blip into a self-inflicted overload. A circuit breaker stops calling the dependency for a cooldown when failure rates cross a threshold, then probes carefully before returning to normal traffic.

This post is a practical TypeScript circuit breaker around DecodeVinValues: states, what counts as failure, UX while open, and how it differs from rate-limit handling and negative caches.

Retry is not a breaker

Pattern Question it answers
Validate locally Is this VIN even eligible to send?
Single-flight Are duplicate in-flight lookups coalesced?
Retry + jitter Should this one transient failure try again?
Circuit breaker Should we stop calling upstream for everyone right now?
Negative cache Should we remember "no match" for this VIN briefly?

Use all of them in layers. The breaker sits outside per-request retries: open the circuit on aggregated failure rate or consecutive failures, not on the first timeout.

States that match product reality

Classic three states work well for public VIN decode:

  1. Closed (normal): requests flow to NHTSA. Track successes and failures in a short window.
  2. Open (tripped): do not call NHTSA. Fail fast with a stable "decode temporarily unavailable" result. Start a cooldown timer.
  3. Half-open (probe): allow a small number of trial requests. If they succeed, close. If they fail, open again.

For a free consumer tool, prefer failing fast while open over queueing users behind a pile of retries. Honest degradation beats a spinner that lies.

What should trip the breaker

Count as failure for breaker math:

  • Network errors and hard timeouts
  • HTTP 502 / 503 / 504
  • HTTP 429 when it arrives in bursts (treat sustained 429 as upstream pressure)
  • Response bodies that are unparseable when you normally expect JSON

Do not count as breaker failures:

  • Local validation rejects (length, charset, check digit)
  • HTTP 200 with sparse Make/Model fields (partial success)
  • Clear "no data" decode rows for a weird VIN (business miss, not outage)

Mixing validation noise into the failure rate trips the breaker during a bad OCR day and blocks good VINs for no reason.

TypeScript sketch

type BreakerState = "closed" | "open" | "half_open";

export type BreakerConfig = {
  failureThreshold: number; // e.g. 5 consecutive, or use rate below
  cooldownMs: number; // e.g. 30_000
  halfOpenMaxProbes: number; // e.g. 2
};

export class NhtsaCircuitBreaker {
  private state: BreakerState = "closed";
  private consecutiveFailures = 0;
  private openedAt = 0;
  private halfOpenProbes = 0;

  constructor(private readonly cfg: BreakerConfig) {}

  canRequest(): boolean {
    if (this.state === "closed") return true;
    if (this.state === "open") {
      if (Date.now() - this.openedAt >= this.cfg.cooldownMs) {
        this.state = "half_open";
        this.halfOpenProbes = 0;
        return true;
      }
      return false;
    }
    // half_open
    return this.halfOpenProbes < this.cfg.halfOpenMaxProbes;
  }

  beforeRequest(): void {
    if (this.state === "half_open") this.halfOpenProbes += 1;
  }

  recordSuccess(): void {
    this.consecutiveFailures = 0;
    this.state = "closed";
    this.halfOpenProbes = 0;
  }

  recordFailure(): void {
    this.consecutiveFailures += 1;
    if (
      this.state === "half_open" ||
      this.consecutiveFailures >= this.cfg.failureThreshold
    ) {
      this.state = "open";
      this.openedAt = Date.now();
      this.halfOpenProbes = 0;
    }
  }
}

export async function decodeWithBreaker<T>(
  breaker: NhtsaCircuitBreaker,
  call: () => Promise<T>,
): Promise<{ ok: true; data: T } | { ok: false; reason: "circuit_open" | "upstream" }> {
  if (!breaker.canRequest()) return { ok: false, reason: "circuit_open" };
  breaker.beforeRequest();
  try {
    const data = await call();
    breaker.recordSuccess();
    return { ok: true, data };
  } catch {
    breaker.recordFailure();
    return { ok: false, reason: "upstream" };
  }
}
Enter fullscreen mode Exit fullscreen mode

Tune failureThreshold and cooldownMs from your traffic. A low-volume hobby tool can use consecutive failures. A busier API should prefer a sliding window failure rate so one user cannot trip the world alone - combine with per-VIN single-flight so duplicates do not inflate counts.

UX while the circuit is open

Show a distinct state, not a fake empty vehicle card:

  • Title: "Vehicle decode is temporarily unavailable"
  • Body: "The upstream NHTSA service is not responding normally. Try again in a minute."
  • Optional: serve stale last-good decode for that VIN if you already have a cache, clearly labeled "cached earlier; live refresh paused"

Do not invent Make/Model. Do not map circuit-open to "invalid VIN." Keep error codes separate (CIRCUIT_OPEN vs VIN_INVALID vs UPSTREAM_5XX) so support and GEO-facing status pages stay accurate.

Observability

Export metrics:

  • breaker_state (gauge/enum)
  • breaker_open_total
  • decode_requests_short_circuited
  • failure rate in the closed window

Alert on time spent open, not on every upstream 503. Pair with rate-limit UX: a breaker protects NHTSA from your retries; client rate limits protect you from abusive paste floods.

Product rules

  • Breaker wraps upstream calls only; validation stays always-on.
  • Open = fail fast + optional stale read; never fabricate specs.
  • Half-open probes are few and counted.
  • Do not trip on sparse 200s or local rejects.
  • Document the dependency: manufacturer attributes come from NHTSA; downtime is possible.

Takeaway

When NHTSA error rates spike, more retries make you part of the outage. A small circuit breaker fails fast, cools down, probes, and returns - while your UI tells the truth about temporary unavailability. Layer it with validation, single-flight, and bounded retries, and free VIN decode stays kind to both users and the public API.

I maintain VIN Lookup, a free VIN decode based on NHTSA data.

Top comments (0)