<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[VIN Lookup Guides]]></title><description><![CDATA[VIN Lookup Guides]]></description><link>https://freevinlookup.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac4da2e8bfa76860e22f743/98f9e243-1f12-4d9f-9394-da02941f57a2.png</url><title>VIN Lookup Guides</title><link>https://freevinlookup.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 11 Oct 2026 06:40:17 GMT</lastBuildDate><atom:link href="https://freevinlookup.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Negatively Caching Invalid VIN Responses So Bad Digits Do Not Hammer NHTSA]]></title><description><![CDATA[A free VIN decode that always forwards every paste to live NHTSA DecodeVinValues pays upstream cost when users typo a check digit, paste seventeen zeros, or retry the same invalid string. Positive ETa]]></description><link>https://freevinlookup.hashnode.dev/negatively-caching-invalid-vin-responses-so-bad-digits-do-not-hammer-nhtsa</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/negatively-caching-invalid-vin-responses-so-bad-digits-do-not-hammer-nhtsa</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Fri, 09 Oct 2026 09:22:42 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode that always forwards every paste to live NHTSA <code>DecodeVinValues</code> pays upstream cost when users typo a check digit, paste seventeen zeros, or retry the same invalid string. Positive ETag caches and make-pattern warmup (covered elsewhere) store <strong>successful</strong> bodies. This post is different: a <strong>negative cache</strong> for VINs you already know are invalid or that vPIC already refused, so bad digits do not hammer NHTSA on every retry.</p>
<p>The goal is honest short-circuit: remember "this normalized VIN is not worth another upstream call for a while," show a clear invalid/error state, and never invent a successful decode card from a negative entry.</p>
<h2>Negative cache vs positive caches</h2>
<p>Use a negative cache when:</p>
<ul>
<li>Client-side checks (length, charset, check digit) already fail before HTTP</li>
<li>Upstream returned a clear "no decode / error / empty Results" for that VIN</li>
<li>The same bad paste repeats within a short TTL (fat-finger retries, bot loops)</li>
</ul>
<p>Do <strong>not</strong> use negative cache to store successful DecodeVinValues bodies (ETag / warmup territory), invent folklore Make/Model/Year, suppress live calls forever (bound TTL), or treat 429/outages as permanent "invalid VIN."</p>
<p>When the entry expires or the failure was transient (5xx, timeout), allow a live retry. Client-invalid shapes can use a longer TTL than soft upstream empties.</p>
<h2>Classify before you store</h2>
<p>Separate <strong>client-invalid</strong> (never call upstream) from <strong>upstream-negative</strong> (you called, got a clear no-decode). Both can live in one map keyed by normalized VIN, with different reasons and TTLs.</p>
<pre><code class="language-ts">export type NegReason =
  | "client-length"
  | "client-charset"
  | "client-check-digit"
  | "upstream-empty"
  | "upstream-error";

export type NegEntry = {
  vinNormalized: string;
  reason: NegReason;
  storedAt: number; // epoch ms
  ttlMs: number;
  message: string; // user-safe, no invented specs
};

export type NegStore = Map&lt;string, NegEntry&gt;;

const CLIENT_TTL_MS = 60 * 60 * 1000; // 1h for obvious typos
const UPSTREAM_TTL_MS = 15 * 60 * 1000; // 15m for empty/error bodies

export function normalizeVin(raw: string): string {
  return raw.trim().toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, "");
}

export function clientInvalidReason(vin: string): NegReason | null {
  if (vin.length !== 17) return "client-length";
  if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(vin)) return "client-charset";
  // Check-digit algorithm omitted for brevity; return "client-check-digit" on fail
  return null;
}

export function putNegative(
  store: NegStore,
  vinNormalized: string,
  reason: NegReason,
  message: string,
  nowMs = Date.now(),
): NegEntry {
  const ttlMs =
    reason.startsWith("client-") ? CLIENT_TTL_MS : UPSTREAM_TTL_MS;
  const entry: NegEntry = {
    vinNormalized,
    reason,
    storedAt: nowMs,
    ttlMs,
    message,
  };
  store.set(vinNormalized, entry);
  return entry;
}

export function getNegative(
  store: NegStore,
  vinNormalized: string,
  nowMs = Date.now(),
): NegEntry | null {
  const hit = store.get(vinNormalized);
  if (!hit) return null;
  if (nowMs - hit.storedAt &gt;= hit.ttlMs) {
    store.delete(vinNormalized);
    return null;
  }
  return hit;
}
</code></pre>
<p>Never put a successful body into this map. Never upgrade a negative hit into Make/Model/Year rows.</p>
<h2>Decode path with short-circuit</h2>
<pre><code class="language-ts">export type DecodeOutcome =
  | { kind: "negative"; entry: NegEntry }
  | { kind: "live"; body: Record&lt;string, string | null&gt; }
  | { kind: "transient-error"; status: number };

export async function decodeWithNegCache(
  store: NegStore,
  rawVin: string,
  fetchLive: (vin: string) =&gt; Promise&lt;{
    ok: boolean;
    status: number;
    body: Record&lt;string, string | null&gt; | null;
    empty: boolean;
  }&gt;,
  nowMs = Date.now(),
): Promise&lt;DecodeOutcome&gt; {
  const vin = normalizeVin(rawVin);
  const cached = getNegative(store, vin, nowMs);
  if (cached) return { kind: "negative", entry: cached };

  const clientReason = clientInvalidReason(vin);
  if (clientReason) {
    const entry = putNegative(
      store,
      vin,
      clientReason,
      "VIN failed local validation -- not sent to NHTSA",
      nowMs,
    );
    return { kind: "negative", entry };
  }

  const live = await fetchLive(vin);
  if (!live.ok &amp;&amp; live.status &gt;= 500) {
    return { kind: "transient-error", status: live.status };
  }
  if (!live.ok || live.empty || !live.body) {
    const entry = putNegative(
      store,
      vin,
      live.ok ? "upstream-empty" : "upstream-error",
      "No decode available from vPIC for this VIN",
      nowMs,
    );
    return { kind: "negative", entry };
  }
  return { kind: "live", body: live.body };
}
</code></pre>
<p>Transient 5xx stays out of the negative store (or uses a very short TTL if you must). That keeps outages from branding a good VIN as permanently invalid.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Serving a fake "success" card from a negative hit so the UI looks complete</li>
<li>Infinite TTL so one empty response never rechecks after catalog updates</li>
<li>Collapsing rate-limit 429 into "invalid VIN" negatives</li>
<li>Mixing negative keys with positive ETag bodies in one undifferentiated blob</li>
</ol>
<p>Refuse those. Negative cache is a courtesy throttle and UX shortcut -- not a second invent-specs path.</p>
<pre><code class="language-ts">export function assertNoNegInvent(moduleSource: string): void {
  const banned = [
    /invent.*(make|model|year)/i,
    /negative.*success.?card/i,
    /forever.?invalid/i,
    /treat.?429.?as.?invalid/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(`Negative cache must not invent decode cards: ${re}`);
    }
  }
}

export function uiMessage(entry: NegEntry): string {
  return entry.message;
}
</code></pre>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"VIN failed local validation -- not sent to NHTSA"</li>
<li>"No decode available from vPIC for this VIN (cached briefly)"</li>
<li>"Temporary upstream error -- try again shortly" for transient failures</li>
</ul>
<p>Avoid Make/Model/Year from memory on negative hits, "invalid forever" without TTL disclosure, and silent success styling on a negative entry.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const store: NegStore = new Map();
const short = normalizeVin("123");
assert.equal(clientInvalidReason(short), "client-length");

const neg = putNegative(
  store,
  short,
  "client-length",
  "VIN failed local validation -- not sent to NHTSA",
  1_000,
);
assert.equal(getNegative(store, short, 1_000)?.reason, "client-length");
assert.equal(getNegative(store, short, 1_000 + CLIENT_TTL_MS)?.reason, undefined);

assert.equal(uiMessage(neg).includes("Make"), false);
assert.ok(!/standard|verified|package/i.test(uiMessage(neg)));
</code></pre>
<p>Review rule: negative-cache modules must not invent successful decode fields. Bound TTL; separate transient errors from invalid VINs.</p>
<h2>Takeaway</h2>
<p>A negative cache stops bad digits and known empty upstream results from hammering NHTSA on every retry. Store reason + TTL, short-circuit with honest UI copy, and leave successful bodies to ETag and warmup paths. Invalid pastes stay remembered briefly -- never upgraded into invented catalog rows.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/negatively-caching-invalid-vin-responses-so-bad-digits-do-not-hammer-nhtsa-2dok">https://dev.to/vin_lookup_8dbd4710f77e9e/negatively-caching-invalid-vin-responses-so-bad-digits-do-not-hammer-nhtsa-2dok</a></p>
]]></content:encoded></item><item><title><![CDATA[Ignoring Stale VIN Decode Responses After a Newer Request Wins the Race]]></title><description><![CDATA[A free VIN decode form often fires DecodeVinValues (or your thin proxy) on every paste or debounce tick. Users type fast: VIN A starts, then VIN B starts before A returns. If you apply every resolved ]]></description><link>https://freevinlookup.hashnode.dev/ignoring-stale-vin-decode-responses-after-a-newer-request-wins-the-race</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/ignoring-stale-vin-decode-responses-after-a-newer-request-wins-the-race</guid><category><![CDATA[cars]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Fri, 09 Oct 2026 09:07:27 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode form often fires DecodeVinValues (or your thin proxy) on every paste or debounce tick. Users type fast: VIN A starts, then VIN B starts before A returns. If you apply every resolved promise, the card can briefly show B's Make/Model and then flip back to A's older payload -- or paint A's error under B's input.</p>
<p>This post is about a <strong>request-id / generation-counter stale-response guard</strong>: bump a monotonic id when you start a decode, stamp it on the in-flight promise, and commit UI state only when that id still matches the latest generation. AbortSignal, input-clear cancel UX, and timeout-overlap races are separate topics. Here the focus is out-of-order success and error payloads after a newer request already owns the screen.</p>
<h2>The failure mode</h2>
<p>Typical buggy sequence:</p>
<ol>
<li>User pastes VIN A; you <code>fetch</code> and keep the promise.</li>
<li>Before A resolves, the user pastes VIN B; you start a second fetch (with or without aborting A).</li>
<li>B returns first; you paint Make/Model for B. Good so far.</li>
<li>A returns later; a careless <code>.then</code> still calls <code>setResult(rowA)</code>.</li>
<li>The input shows B, the card shows A's identity. Support tickets look like "decode lies."</li>
</ol>
<p>Aborting A helps when cancel is honored. It fails when the proxy already finished, a shared promise lacks a generation stamp, or you only abort on empty input. A generation counter (or UUID request id) is the last line of defense: late bodies must be ignored when no longer current.</p>
<h2>One generation per decode start</h2>
<p>Bump <code>generation</code> (or mint a <code>requestId</code>) every time you start a decode for a normalized VIN. Store the stamp on the session. Every success and failure path checks <code>session.generation === latestGeneration</code> before mutating result, error, or loading.</p>
<pre><code class="language-ts">export type DecodeRow = Record&lt;string, string&gt;;

export type DecodeUiState = {
  vin: string | null;
  row: DecodeRow | null;
  error: string | null;
  loading: boolean;
  generation: number;
};

export type DecodeSession = {
  generation: number;
  vinNormalized: string;
};

export function nextGeneration(state: DecodeUiState): number {
  return state.generation + 1;
}

export function isCurrent(
  session: DecodeSession,
  latestGeneration: number,
): boolean {
  return session.generation === latestGeneration;
}

export function applyDecodeSuccess(
  state: DecodeUiState,
  session: DecodeSession,
  row: DecodeRow,
): DecodeUiState {
  if (!isCurrent(session, state.generation)) {
    return state; // stale -- newer request already owns the UI
  }
  return {
    ...state,
    vin: session.vinNormalized,
    row,
    error: null,
    loading: false,
  };
}

export function applyDecodeError(
  state: DecodeUiState,
  session: DecodeSession,
  error: string,
): DecodeUiState {
  if (!isCurrent(session, state.generation)) {
    return state;
  }
  return {
    ...state,
    vin: session.vinNormalized,
    row: null,
    error,
    loading: false,
  };
}
</code></pre>
<p>The guard is deliberately boring. Stale successes and stale errors both refuse to write. A late "404 from NHTSA" for A must not wipe B's good row. Treat "current generation" as the single source of truth for which VIN owns the card -- not whichever promise happens to settle last.</p>
<h2>Wire it at the promise boundary</h2>
<p>Start the session when you kick off work. Pass the same session into both branches. Do not close over a mutable <code>let result</code> without the check.</p>
<pre><code class="language-ts">export async function runDecode(
  state: DecodeUiState,
  vinNormalized: string,
  fetchRow: (vin: string) =&gt; Promise&lt;DecodeRow&gt;,
): Promise&lt;DecodeUiState&gt; {
  const generation = nextGeneration(state);
  const session: DecodeSession = { generation, vinNormalized };
  let next: DecodeUiState = {
    ...state,
    generation,
    loading: true,
    error: null,
  };

  try {
    const row = await fetchRow(vinNormalized);
    next = applyDecodeSuccess(next, session, row);
  } catch (err) {
    const message = err instanceof Error ? err.message : "Decode failed";
    next = applyDecodeError(next, session, message);
  }
  return next;
}
</code></pre>
<p>If setState lands after another start bumped <code>generation</code>, <code>applyDecodeSuccess</code> no-ops. Product guarantee: <strong>newer request wins; older responses become no-ops.</strong></p>
<h2>What this is not</h2>
<p>Do not conflate this guard with AbortSignal plumbing, input-clear cancel UX, timeout-only races, or SWR cache revalidation. Those are adjacent. You can combine abort + generation: abort reduces wasted work; generation prevents wrong paint when abort is incomplete.</p>
<h2>Forbidden "fixes"</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>"Just always take the last response that arrives" without comparing ids (that is the bug)</li>
<li>Showing a spinner until <em>all</em> in-flight promises settle (hangs the UI on abandoned A)</li>
<li>Merging row A and row B fields because "some data is better than none"</li>
<li>Skipping the guard for errors ("errors are harmless") -- they are not; they blank good cards</li>
<li>Reusing one global promise for every VIN without stamping which VIN it belongs to</li>
</ol>
<p>Refuse those. One stamp per start; commit only if current. The expensive bug is not a wasted network call -- it is a trustworthy-looking card that silently shows the wrong vehicle.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const base: DecodeUiState = {
  vin: null,
  row: null,
  error: null,
  loading: false,
  generation: 0,
};

const sessionA: DecodeSession = { generation: 1, vinNormalized: "A".repeat(17) };
const sessionB: DecodeSession = { generation: 2, vinNormalized: "B".repeat(17) };

let state: DecodeUiState = { ...base, generation: 2, loading: true };
state = applyDecodeSuccess(state, sessionB, { Make: "Beta" });
assert.equal(state.row?.Make, "Beta");

// Late A must not overwrite B
state = applyDecodeSuccess(state, sessionA, { Make: "Alpha" });
assert.equal(state.row?.Make, "Beta");

// Late A error must not wipe B
state = applyDecodeError(state, sessionA, "upstream timeout");
assert.equal(state.row?.Make, "Beta");
assert.equal(state.error, null);

assert.equal(isCurrent(sessionA, 2), false);
assert.equal(isCurrent(sessionB, 2), true);
</code></pre>
<p>Review rule: decode commit paths must ignore mismatched generations. Never apply a resolved body solely because <code>loading</code> was once true.</p>
<h2>Takeaway</h2>
<p>Out-of-order VIN decode responses are normal on a chatty form. Stamp every start with a request id or generation counter, and ignore successes and errors that no longer match. Your free VIN UI stays trustworthy when the newest request owns the card -- and older payloads become silent no-ops instead of identity flip-flops.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/ignoring-stale-vin-decode-responses-after-a-newer-request-wins-the-race-4j99">https://dev.to/vin_lookup_8dbd4710f77e9e/ignoring-stale-vin-decode-responses-after-a-newer-request-wins-the-race-4j99</a></p>
]]></content:encoded></item><item><title><![CDATA[Debouncing VIN Input So Partial Digits Do Not Hammer NHTSA]]></title><description><![CDATA[A free VIN decode form that fires DecodeVinValues (or your thin proxy) on every keystroke will hammer NHTSA while the user is still typing digits 1 through 16. Paste-and-go users are fine; hunt-and-pe]]></description><link>https://freevinlookup.hashnode.dev/debouncing-vin-input-so-partial-digits-do-not-hammer-nhtsa</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/debouncing-vin-input-so-partial-digits-do-not-hammer-nhtsa</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[UX]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Fri, 09 Oct 2026 08:46:25 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode form that fires DecodeVinValues (or your thin proxy) on every keystroke will hammer NHTSA while the user is still typing digits 1 through 16. Paste-and-go users are fine; hunt-and-peck users generate a burst of incomplete candidates that never should have left the browser.</p>
<p>This post is about <strong>debounce-before-request</strong>: wait until typing pauses before you start a decode for a still-settling VIN, and only schedule work when the normalized value is a full 17-character candidate. Input-clear cancel (#131 family) decides when to <em>stop</em> an in-flight request. Stale-response guards decide which response may <em>commit</em>. Here the focus is delaying the <em>start</em> so partial digits never become upstream traffic.</p>
<h2>The failure mode</h2>
<p>Typical sequence without debounce-before-request:</p>
<ol>
<li>User types character by character toward a 17-character VIN.</li>
<li>Your handler treats length &gt;= 17 (or even length == 17 after a typo-fix) as "ready" and fires immediately on the first full-looking string.</li>
<li>The user backspaces digit 17, types a correction, and you fire again -- sometimes three times in under a second.</li>
<li>NHTSA (or your proxy quota) sees a burst of near-identical incomplete or wrong VINs.</li>
<li>The UI flickers loading states for candidates the user never intended to submit.</li>
</ol>
<p>Cancel-on-clear does not prevent the burst: each intermediate full-looking string already started. Stale-response ignore only drops late commits; it does not reduce upstream calls that already left the browser.</p>
<h2>Debounce the schedule, not the paste</h2>
<p>Own one timer per input session. On every change, normalize the VIN. If it is not a decodeable 17-character candidate, clear any pending timer and do not start. If it is decodeable, <strong>reschedule</strong> a single delayed start instead of calling fetch immediately. A paste of a complete VIN still waits the short debounce once -- or you may short-circuit paste with an immediate path; the important part is that mid-typing corrections collapse into one scheduled call.</p>
<pre><code class="language-ts">const VIN_RE = /^[A-HJ-NPR-Z0-9]{17}$/;

export function normalizeVin(raw: string): string {
  return raw.trim().toUpperCase().replace(/[\s\-._]/g, "");
}

export function shouldDecode(normalized: string): boolean {
  return VIN_RE.test(normalized);
}

export type DebounceHandle = {
  timer: ReturnType&lt;typeof setTimeout&gt; | null;
  generation: number;
};

export function createDebounceHandle(): DebounceHandle {
  return { timer: null, generation: 0 };
}

export function scheduleDecode(opts: {
  raw: string;
  handle: DebounceHandle;
  delayMs: number;
  startDecode: (vin: string, generation: number) =&gt; void;
}): void {
  const next = normalizeVin(opts.raw);
  if (opts.handle.timer != null) {
    clearTimeout(opts.handle.timer);
    opts.handle.timer = null;
  }

  if (!shouldDecode(next)) {
    // Partial digits: never start; do not leave a pending timer.
    return;
  }

  const generation = ++opts.handle.generation;
  opts.handle.timer = setTimeout(() =&gt; {
    opts.handle.timer = null;
    opts.startDecode(next, generation);
  }, opts.delayMs);
}
</code></pre>
<p>Partial lengths never schedule. Full candidates schedule once; each new keystroke resets the timer so only the pause after the last edit becomes a request.</p>
<h2>Pair with generation, not only AbortController</h2>
<p>When the timer fires, pass a generation (or the exact VIN string) into the fetch path. If a newer schedule already bumped <code>generation</code>, ignore the stale start. This is complementary to AbortSignal: debounce reduces how often you <em>create</em> controllers; abort and stale guards clean up what already started.</p>
<pre><code class="language-ts">export type DecodeSession = {
  controller: AbortController;
  vinNormalized: string;
  generation: number;
};

export function startIfCurrent(opts: {
  vin: string;
  generation: number;
  handle: DebounceHandle;
  session: DecodeSession | null;
  setSession: (s: DecodeSession | null) =&gt; void;
  fetchDecode: (vin: string, signal: AbortSignal) =&gt; Promise&lt;unknown&gt;;
}): void {
  if (opts.generation !== opts.handle.generation) return;

  opts.session?.controller.abort();
  const controller = new AbortController();
  opts.setSession({
    controller,
    vinNormalized: opts.vin,
    generation: opts.generation,
  });

  void opts.fetchDecode(opts.vin, controller.signal);
}
</code></pre>
<p>Do not fire on every <code>input</code> event for length 1-16. Do not treat debounce as a substitute for clear-cancel: if the user empties the box after a timer was scheduled, clear the timer <em>and</em> abort any session that already started.</p>
<h2>What this is not</h2>
<ul>
<li><strong>Not</strong> input-clear cancel: that aborts when the box becomes empty mid-flight.</li>
<li><strong>Not</strong> stale-response ignore: that drops commits when a newer request won the race.</li>
<li><strong>Not</strong> "debounce complete pastes forever": paste of a valid 17-character VIN may use a shorter delay or immediate fire; the hammering problem is partial and rapidly corrected digits.</li>
</ul>
<p>You can combine debounce-before-request, clear-cancel, and stale guards. Debounce is the gate that keeps incomplete typing off the wire.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const calls: string[] = [];
const handle = createDebounceHandle();

scheduleDecode({
  raw: "1HGCM82633A00435", // 16 chars
  handle,
  delayMs: 300,
  startDecode: (vin) =&gt; calls.push(vin),
});
assert.equal(handle.timer, null);
assert.deepEqual(calls, []);

scheduleDecode({
  raw: "1HGCM82633A004352",
  handle,
  delayMs: 300,
  startDecode: (vin) =&gt; calls.push(vin),
});
assert.ok(handle.timer != null);

scheduleDecode({
  raw: "1HGCM82633A004353", // corrected last digit
  handle,
  delayMs: 300,
  startDecode: (vin) =&gt; calls.push(vin),
});
assert.ok(handle.timer != null);

// Simulate timer fire for the latest schedule only.
const gen = handle.generation;
if (handle.timer) {
  clearTimeout(handle.timer);
  handle.timer = null;
}
startIfCurrent({
  vin: "1HGCM82633A004353",
  generation: gen,
  handle,
  session: null,
  setSession: () =&gt; {},
  fetchDecode: async (vin) =&gt; {
    calls.push(`fetch:${vin}`);
    return {};
  },
});
assert.deepEqual(calls, ["fetch:1HGCM82633A004353"]);
</code></pre>
<p>Review rule: partial VINs must not schedule upstream work; rapid corrections must collapse into one delayed start.</p>
<h2>Choosing a delay</h2>
<p>A delay in the 250-400ms range usually absorbs hunt-and-peck corrections without feeling sticky on paste. Measure your audience: if most users paste a full VIN, prefer the short end (or an immediate path when <code>inputType</code> indicates insertFromPaste). If most users type digit-by-digit on mobile, the longer end collapses more accidental 17-character flashes. Log scheduled-versus-fired counts in staging to see how often the timer saved an upstream call.</p>
<h2>Takeaway</h2>
<p>Debounce VIN input <strong>before</strong> you request DecodeVinValues so partial digits and rapid corrections never hammer NHTSA. Schedule only decodeable 17-character candidates, reset the timer on each edit, and pair the fire with a generation or AbortController so stale starts cannot pile up. Keep this gate separate from clear-cancel and stale-response guards. Your free VIN form stays kind to upstream quotas when typing noise stays in the browser until the user actually pauses on a full VIN.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/debouncing-vin-input-so-partial-digits-do-not-hammer-nhtsa-82n">https://dev.to/vin_lookup_8dbd4710f77e9e/debouncing-vin-input-so-partial-digits-do-not-hammer-nhtsa-82n</a></p>
]]></content:encoded></item><item><title><![CDATA[Gracefully Degrading Secondary VIN Enrichment When NHTSA Latency Spikes]]></title><description><![CDATA[A free VIN decode card often layers a critical DecodeVinValues body with secondary enrichers -- recall snippets, stolen-check stubs, photo hints, or analytics side calls. When NHTSA latency spikes, wa]]></description><link>https://freevinlookup.hashnode.dev/gracefully-degrading-secondary-vin-enrichment-when-nhtsa-latency-spikes</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/gracefully-degrading-secondary-vin-enrichment-when-nhtsa-latency-spikes</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Thu, 08 Oct 2026 09:17:27 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode card often layers a critical DecodeVinValues body with secondary enrichers -- recall snippets, stolen-check stubs, photo hints, or analytics side calls. When NHTSA latency spikes, waiting for every enricher makes the whole card feel broken. Load shedding (covered elsewhere) rejects or drops work at the edge to protect capacity. Circuit breakers (elsewhere) open on failure rates. Concurrency limits and retry budgets (elsewhere) bound sockets and attempts. This post is different: <strong>graceful degradation</strong> -- still return the core vPIC decode on time, mark secondary enrichers as skipped/degraded with honest UI labels, and never invent enrichment rows to keep the card looking "complete."</p>
<p>The goal is narrow: under a latency deadline, finish primary decode first; cancel or skip non-critical enrichers; show partial results with clear provenance; refuse fake filler specs.</p>
<h2>Graceful degrade vs load-shed, circuits, concurrency, retry budget</h2>
<ul>
<li><strong>Load shed</strong> -- refuse or drop requests when the edge is overloaded</li>
<li><strong>Circuit breaker</strong> -- stop calling an unhealthy dependency globally for a cool-down</li>
<li><strong>Concurrency / retry budget</strong> -- cap in-flight calls and per-VIN attempts</li>
<li><strong>Graceful degrade</strong> -- keep serving the <em>critical</em> decode path; secondary enrichers become optional under a remaining-time budget</li>
</ul>
<p>Degrade is about <em>partial success for one request</em>. Shed is about <em>protecting the fleet</em>. You can degrade without shedding, and shed without offering a partial card.</p>
<h2>Priority tiers and a remaining-time budget</h2>
<p>Assign each step a priority. Primary DecodeVinValues is required. Secondary enrichers run only while remaining deadline allows. On skip, record reason -- never fabricate their fields.</p>
<pre><code class="language-ts">export type EnricherId =
  | "vpic-primary"
  | "recall-snippet"
  | "photo-hint"
  | "analytics-ping";

export type EnricherResult =
  | { id: EnricherId; status: "ok"; data: Record&lt;string, string&gt; }
  | { id: EnricherId; status: "skipped"; reason: "deadline" | "dependency" }
  | { id: EnricherId; status: "error"; message: string };

export type Deadline = { startMs: number; budgetMs: number };

export function remainingMs(d: Deadline, now = Date.now()): number {
  return d.budgetMs - (now - d.startMs);
}

export type Enricher = {
  id: EnricherId;
  critical: boolean;
  minMs: number; // bail if remaining below this
  run: (vin: string, signal: AbortSignal) =&gt; Promise&lt;Record&lt;string, string&gt;&gt;;
};

export async function runWithGracefulDegrade(
  vin: string,
  enrichers: Enricher[],
  deadline: Deadline,
): Promise&lt;EnricherResult[]&gt; {
  const out: EnricherResult[] = [];
  for (const e of enrichers) {
    const left = remainingMs(deadline);
    if (!e.critical &amp;&amp; left &lt; e.minMs) {
      out.push({ id: e.id, status: "skipped", reason: "deadline" });
      continue;
    }
    const ac = new AbortController();
    const timer = setTimeout(
      () =&gt; ac.abort(),
      Math.max(0, e.critical ? left : Math.min(left, e.minMs * 2)),
    );
    try {
      const data = await e.run(vin, ac.signal);
      out.push({ id: e.id, status: "ok", data });
    } catch (err) {
      if (e.critical) {
        out.push({
          id: e.id,
          status: "error",
          message: err instanceof Error ? err.message : "primary failed",
        });
        break;
      }
      out.push({ id: e.id, status: "skipped", reason: "deadline" });
    } finally {
      clearTimeout(timer);
    }
  }
  return out;
}

export type CardRow = { label: string; value: string };

export function cardFromResults(results: EnricherResult[]): CardRow[] {
  const rows: CardRow[] = [];
  for (const r of results) {
    if (r.status === "ok") {
      for (const [k, v] of Object.entries(r.data)) {
        rows.push({ label: k, value: v });
      }
      continue;
    }
    if (r.id === "vpic-primary" &amp;&amp; r.status !== "ok") {
      rows.push({ label: "Decode", value: "primary decode unavailable" });
      continue;
    }
    // Secondary: honest skip -- never invent enrichment values
    rows.push({
      label: r.id,
      value:
        r.status === "skipped"
          ? `skipped (${r.reason}) -- not provided this request`
          : `error -- not provided this request`,
    });
  }
  return rows;
}
</code></pre>
<p>Primary failure fails the request (or returns a clear error). Secondary skips leave the core Make/Model/Year rows intact when primary succeeded.</p>
<h2>UI honesty under degrade</h2>
<p>Prefer:</p>
<ul>
<li>Core vPIC rows from a successful primary</li>
<li>"Recall snippet: skipped (deadline) -- not provided this request"</li>
<li>A banner: "Partial decode -- secondary enrichment deferred"</li>
</ul>
<p>Avoid:</p>
<ul>
<li>Grey fake recall counts or "no recalls" invented on skip</li>
<li>Hiding that photo-hint never ran</li>
<li>Relabeling a degraded card as "full enrichment complete"</li>
</ul>
<h2>Forbidden upgrades</h2>
<ol>
<li>Inventing secondary enrichment fields when skipped for deadline</li>
<li>Blocking the whole card on non-critical enrichers after primary succeeded</li>
<li>Treating graceful degrade as a license to ignore concurrency / retry budgets</li>
<li>Opening the circuit solely because one enricher skipped (different tool)</li>
<li>Shedding healthy primary decodes when you only needed to skip analytics</li>
</ol>
<p>Refuse those. Partial + labeled beats complete + fake.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const enrichers: Enricher[] = [
  {
    id: "vpic-primary",
    critical: true,
    minMs: 50,
    run: async () =&gt; ({ Make: "HONDA", Model: "Accord" }),
  },
  {
    id: "recall-snippet",
    critical: false,
    minMs: 80,
    run: async (_v, signal) =&gt; {
      await new Promise((r, j) =&gt; {
        const t = setTimeout(r, 200);
        signal.addEventListener("abort", () =&gt; {
          clearTimeout(t);
          j(new Error("aborted"));
        });
      });
      return { recalls: "0" };
    },
  },
];

const results = await runWithGracefulDegrade("1HGCM82633A004352", enrichers, {
  startMs: Date.now(),
  budgetMs: 100,
});
assert.ok(results.some((r) =&gt; r.id === "vpic-primary" &amp;&amp; r.status === "ok"));
assert.ok(
  results.some(
    (r) =&gt; r.id === "recall-snippet" &amp;&amp; r.status === "skipped",
  ),
);
const rows = cardFromResults(results);
assert.ok(rows.some((r) =&gt; r.label === "Make" &amp;&amp; r.value === "HONDA"));
assert.ok(
  rows.some((r) =&gt; /skipped|not provided this request/i.test(r.value)),
);
assert.ok(!rows.some((r) =&gt; r.label === "recalls" &amp;&amp; r.value === "0"));
</code></pre>
<p>Review rule: degrade modules must preserve primary success and must not invent secondary fields on skip.</p>
<h2>Takeaway</h2>
<p>Graceful degradation keeps the critical DecodeVinValues path on time when NHTSA latency spikes, while secondary enrichers skip under a remaining-time budget with honest labels. Pair it with load shedding, circuits, concurrency limits, and retry budgets -- each owns a different pressure valve. Your free VIN card stays trustworthy when partial results are labeled partial -- never padded with invented enrichment marketing.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/gracefully-degrading-secondary-vin-enrichment-when-nhtsa-latency-spikes-1270">https://dev.to/vin_lookup_8dbd4710f77e9e/gracefully-degrading-secondary-vin-enrichment-when-nhtsa-latency-spikes-1270</a></p>
]]></content:encoded></item><item><title><![CDATA[Propagating Deadlines from VIN Form Submit to NHTSA Proxy Without Orphaned Work]]></title><description><![CDATA[A free VIN form that abandons a slow decode still often leaves a DecodeVinValues call running behind the proxy. The user navigated away, a client timeout fired, or a newer submit superseded the old on]]></description><link>https://freevinlookup.hashnode.dev/propagating-deadlines-from-vin-form-submit-to-nhtsa-proxy-without-orphaned-work</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/propagating-deadlines-from-vin-form-submit-to-nhtsa-proxy-without-orphaned-work</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Thu, 08 Oct 2026 08:58:31 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN form that abandons a slow decode still often leaves a DecodeVinValues call running behind the proxy. The user navigated away, a client timeout fired, or a newer submit superseded the old one -- yet the edge kept waiting on NHTSA and burned quota on a response nobody will display. Per-request budgets (covered elsewhere) decide how long a serverless handler may wait. This post is about <strong>propagating</strong> that deadline as an AbortSignal from form submit through your BFF so orphaned upstream work is cancelled early.</p>
<p>The goal is narrow: one submit, one deadline, one cancellable fetch chain. Do not confuse this with hedged dual-fire, idempotency keys, or jittered retries. Here the job is: when the UI no longer wants the result, stop waiting on the proxy path that would call NHTSA.</p>
<h2>Where orphaned work comes from</h2>
<p>Orphans appear when deadlines live only at one layer:</p>
<ul>
<li>Browser <code>AbortController</code> aborts the BFF fetch, but the BFF ignores <code>req.signal</code> and keeps calling DecodeVinValues</li>
<li>BFF has a hard <code>setTimeout</code> that returns 504 to the client while the outbound NHTSA promise is still pending</li>
<li>A second form submit starts a new decode without aborting the first in-flight attempt</li>
<li>Serverless budgets shrink remaining time, but nobody wires that remaining budget into <code>fetch(..., { signal })</code></li>
</ul>
<p>Each case spends free-quota units on work the card will never show. Propagation means the same abort reason travels from submit handler to outbound proxy call.</p>
<h2>Mint a deadline at submit, pass the signal</h2>
<p>Create the controller when the user submits (or when the form auto-decodes once). Attach a wall-clock deadline, pass <code>signal</code> to your BFF, and abort on unmount, navigation, or superseding submit.</p>
<pre><code class="language-ts">export type DecodeSubmitOpts = {
  vin: string;
  deadlineMs: number; // e.g. 8_000 from UX budget
  fetchProxy: (vin: string, signal: AbortSignal) =&gt; Promise&lt;unknown&gt;;
};

export function submitDecodeWithDeadline(
  opts: DecodeSubmitOpts,
): { promise: Promise&lt;unknown&gt;; abort: (reason?: unknown) =&gt; void } {
  const ctrl = new AbortController();
  const timer = setTimeout(() =&gt; {
    ctrl.abort(new Error("CLIENT_DEADLINE"));
  }, opts.deadlineMs);

  const promise = opts
    .fetchProxy(opts.vin, ctrl.signal)
    .finally(() =&gt; clearTimeout(timer));

  return {
    promise,
    abort: (reason) =&gt; ctrl.abort(reason ?? new Error("CLIENT_CANCEL")),
  };
}

export function supersedePrevious(
  previous: { abort: (reason?: unknown) =&gt; void } | null,
): void {
  previous?.abort(new Error("SUPERSEDED"));
}
</code></pre>
<p>On a second tap, abort the previous handle before minting a new one. Do not let two proxy calls race "for completeness" when only the latest submit should paint the card.</p>
<h2>Proxy: forward the inbound signal to NHTSA</h2>
<p>Your BFF must treat the inbound request signal as authoritative. Compose it with any remaining serverless budget so either expiry aborts the outbound DecodeVinValues fetch.</p>
<pre><code class="language-ts">export function mergeAbortSignals(
  ...signals: AbortSignal[]
): AbortSignal {
  const ctrl = new AbortController();
  for (const s of signals) {
    if (s.aborted) {
      ctrl.abort(s.reason);
      return ctrl.signal;
    }
    s.addEventListener(
      "abort",
      () =&gt; ctrl.abort(s.reason),
      { once: true },
    );
  }
  return ctrl.signal;
}

export async function proxyDecodeVin(args: {
  vin: string;
  inbound: AbortSignal;
  budgetSignal: AbortSignal;
  decodeVinValues: (vin: string, signal: AbortSignal) =&gt; Promise&lt;unknown&gt;;
}): Promise&lt;unknown&gt; {
  const signal = mergeAbortSignals(args.inbound, args.budgetSignal);
  if (signal.aborted) {
    throw signal.reason ?? new Error("ABORTED_BEFORE_UPSTREAM");
  }
  return args.decodeVinValues(args.vin, signal);
}
</code></pre>
<p>If the client already aborted, do <strong>not</strong> open a new upstream socket "to populate cache anyway." Cache warming is a separate, explicitly scheduled job -- not a side effect of a cancelled user decode.</p>
<p>Budgets and propagation compose: the serverless remaining-time check decides whether a retry is allowed; the AbortSignal decides whether work already in flight should stop. Propagating a deadline without aborting outbound fetch only returns early to the browser while NHTSA keeps running -- the orphan problem this post targets.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Ignoring client aborts so the proxy "finishes for analytics"</li>
<li>Returning a synthetic 200 after abort by merging a partial body</li>
<li>Letting superseded submits complete in the background without metrics</li>
<li>Treating deadline abort as a retry trigger (that belongs in backoff modules)</li>
<li>Counting only displayed responses against quota while aborted upstreams stay invisible</li>
</ol>
<p>Refuse those. Log <code>decode_aborted_client</code>, <code>decode_aborted_budget</code>, and <code>decode_superseded</code>. Count <strong>issued</strong> DecodeVinValues calls, including ones cancelled after the request left your edge.</p>
<h2>Ops copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>Metrics that separate user cancel, deadline, and supersede</li>
<li>Proxy logs that record whether outbound fetch received an AbortSignal</li>
<li>A kill switch that fails closed (reject new submits) when abort rate spikes with dual open upstreams</li>
</ul>
<p>Avoid:</p>
<ul>
<li>Silent background completion after the UI showed a timeout</li>
<li>Alerting only on HTTP 5xx while orphans inflate NHTSA traffic</li>
<li>Documenting "we always finish the decode" as a reliability feature when the card is gone</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const calls: string[] = [];
const { promise, abort } = submitDecodeWithDeadline({
  vin: "1HGCM82633A004352",
  deadlineMs: 50,
  fetchProxy: async (_vin, signal) =&gt; {
    calls.push("start");
    await new Promise&lt;void&gt;((resolve, reject) =&gt; {
      const t = setTimeout(() =&gt; resolve(), 500);
      signal.addEventListener("abort", () =&gt; {
        clearTimeout(t);
        reject(signal.reason);
      });
    });
    calls.push("done");
    return {};
  },
});

await assert.rejects(promise);
assert.ok(!calls.includes("done"));

const inbound = AbortSignal.abort(new Error("CLIENT_CANCEL"));
await assert.rejects(
  proxyDecodeVin({
    vin: "1HGCM82633A004352",
    inbound,
    budgetSignal: new AbortController().signal,
    decodeVinValues: async () =&gt; {
      calls.push("upstream");
      return {};
    },
  }),
);
assert.ok(!calls.includes("upstream"));
</code></pre>
<p>Review rule: proxy decode modules must pass a merged AbortSignal into outbound NHTSA fetch and must not start upstream work when the inbound signal is already aborted.</p>
<h2>Takeaway</h2>
<p>Deadline propagation turns a form-level cancel into a cancellable NHTSA proxy call. Without it, client timeouts and superseded submits leave orphaned DecodeVinValues work that burns free quota for a card nobody will see. Mint the AbortController at submit, forward the signal through the BFF, merge it with any serverless budget, and refuse background "finish anyway" completions. Keep this path separate from hedges and idempotency keys so abandoned VIN lookups stop spending upstream time as soon as the UI stops caring.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/propagating-deadlines-from-vin-form-submit-to-nhtsa-proxy-without-orphaned-work-48l2">https://dev.to/vin_lookup_8dbd4710f77e9e/propagating-deadlines-from-vin-form-submit-to-nhtsa-proxy-without-orphaned-work-48l2</a></p>
]]></content:encoded></item><item><title><![CDATA[Bulkheading VIN Decode Workers So One Bad VIN Path Cannot Starve the Pool]]></title><description><![CDATA[A free VIN decode proxy often shares one worker pool across every DecodeVinValues call. One pathological path -- a hung upstream, a retry storm on a bad VIN, or a client that floods the same edge -- c]]></description><link>https://freevinlookup.hashnode.dev/bulkheading-vin-decode-workers-so-one-bad-vin-path-cannot-starve-the-pool</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/bulkheading-vin-decode-workers-so-one-bad-vin-path-cannot-starve-the-pool</guid><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Thu, 08 Oct 2026 08:41:07 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode proxy often shares one worker pool across every DecodeVinValues call. One pathological path -- a hung upstream, a retry storm on a bad VIN, or a client that floods the same edge -- can exhaust concurrency and leave healthy submissions waiting behind the pile-up. Per-request deadlines and hedged fetches (covered elsewhere) limit how long a single attempt lives. This post is about <strong>bulkheading</strong>: partitioning concurrency so one bad VIN path cannot starve the rest of the pool.</p>
<p>The goal is narrow: isolate in-flight decode slots by tenant, route, or failure class so a saturated bulkhead rejects or queues locally instead of consuming every shared worker. Do not confuse this with circuit breakers that open on error rates, idempotency keys, or jittered retries. Here the job is: cap how many concurrent NHTSA-bound calls one noisy path may hold.</p>
<h2>Where pool starvation comes from</h2>
<p>Starvation appears when every decode shares one unbounded (or globally capped) queue:</p>
<ul>
<li>A single API key or browser session opens dozens of parallel DecodeVinValues calls</li>
<li>A stuck upstream keeps workers occupied until client timeouts pile retries on top</li>
<li>Batch import and interactive form traffic share the same pool with no separate ceilings</li>
<li>A "bad VIN" hot path retries forever while interactive users wait for free slots</li>
</ul>
<p>Each case turns free-quota and worker time into a tragedy of the commons. Bulkheads give each path its own concurrency budget so exhaustion stays local.</p>
<h2>Cap concurrency per bulkhead key</h2>
<p>Assign each decode a bulkhead key (tenant id, route name, or <code>interactive</code> vs <code>batch</code>). Acquire a slot before calling the proxy; release it in <code>finally</code>. When the bulkhead is full, fail fast or queue with a short local timeout -- do not steal slots from other keys.</p>
<pre><code class="language-ts">export type Bulkhead = {
  key: string;
  limit: number;
  inFlight: number;
  waiters: Array&lt;() =&gt; void&gt;;
};

export function createBulkhead(key: string, limit: number): Bulkhead {
  return { key, limit, inFlight: 0, waiters: [] };
}

export async function withBulkhead&lt;T&gt;(
  bh: Bulkhead,
  run: () =&gt; Promise&lt;T&gt;,
): Promise&lt;T&gt; {
  if (bh.inFlight &gt;= bh.limit) {
    await new Promise&lt;void&gt;((resolve, reject) =&gt; {
      const timer = setTimeout(() =&gt; {
        const i = bh.waiters.indexOf(wake);
        if (i &gt;= 0) bh.waiters.splice(i, 1);
        reject(new Error("BULKHEAD_TIMEOUT"));
      }, 2_000);
      const wake = () =&gt; {
        clearTimeout(timer);
        resolve();
      };
      bh.waiters.push(wake);
    });
  }
  bh.inFlight += 1;
  try {
    return await run();
  } finally {
    bh.inFlight -= 1;
    const next = bh.waiters.shift();
    if (next) next();
  }
}

export function bulkheadKey(opts: {
  tenantId?: string;
  route: "interactive" | "batch";
}): string {
  return `${opts.route}:${opts.tenantId ?? "anon"}`;
}
</code></pre>
<p>Interactive form traffic can use a higher per-tenant limit than batch imports. Anonymous clients get a small shared ceiling so one scraper cannot monopolize DecodeVinValues.</p>
<h2>Forbidden "fixes"</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Raising the global worker limit instead of partitioning noisy paths</li>
<li>Letting batch jobs borrow interactive slots when the batch bulkhead is full</li>
<li>Retrying <code>BULKHEAD_TIMEOUT</code> immediately without backoff (which re-saturates the same key)</li>
<li>Sharing one bulkhead across all tenants "for simplicity"</li>
<li>Treating bulkhead rejection as an NHTSA outage and opening a global breaker</li>
</ol>
<p>Refuse those. A full bulkhead is a local capacity signal, not proof that vPIC is down. Keep breaker logic on upstream error rates; keep bulkheads on concurrency ownership.</p>
<pre><code class="language-ts">export type DecodePool = Map&lt;string, Bulkhead&gt;;

export function getBulkhead(
  pool: DecodePool,
  key: string,
  limits: { interactive: number; batch: number },
): Bulkhead {
  let bh = pool.get(key);
  if (!bh) {
    const route = key.startsWith("batch:") ? "batch" : "interactive";
    bh = createBulkhead(
      key,
      route === "batch" ? limits.batch : limits.interactive,
    );
    pool.set(key, bh);
  }
  return bh;
}

export async function decodeBehindBulkhead&lt;T&gt;(
  pool: DecodePool,
  opts: { tenantId?: string; route: "interactive" | "batch" },
  run: () =&gt; Promise&lt;T&gt;,
): Promise&lt;T&gt; {
  const key = bulkheadKey(opts);
  const bh = getBulkhead(pool, key, { interactive: 8, batch: 2 });
  return withBulkhead(bh, run);
}
</code></pre>
<p>Separate interactive and batch ceilings. Never let a saturated batch key drain the interactive pool, and never map bulkhead rejection into a fabricated "NHTSA unavailable" banner when other bulkheads still succeed.</p>
<h2>UI and API copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"Decode busy for this session -- try again shortly" on <code>BULKHEAD_TIMEOUT</code></li>
<li>Metrics labeled by bulkhead key, not a single global "queue depth"</li>
<li>A short footnote in ops docs: local concurrency cap, not an upstream grade</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"NHTSA is down" when only one tenant bulkhead is full</li>
<li>Silently queueing batch work on interactive workers</li>
<li>Grey spinners that look like a decode in flight after you already rejected the slot</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const bh = createBulkhead("interactive:t1", 1);
let released = false;
const first = withBulkhead(bh, async () =&gt; {
  await new Promise((r) =&gt; setTimeout(r, 50));
  released = true;
  return "ok";
});

await assert.rejects(
  withBulkhead(bh, async () =&gt; "nope"),
  /BULKHEAD_TIMEOUT/,
);
assert.equal(released, false);
assert.equal(await first, "ok");
assert.equal(bh.inFlight, 0);

assert.equal(
  bulkheadKey({ tenantId: "a", route: "batch" }),
  "batch:a",
);

const pool: DecodePool = new Map();
const interactive = getBulkhead(pool, "interactive:x", {
  interactive: 8,
  batch: 2,
});
const batch = getBulkhead(pool, "batch:x", { interactive: 8, batch: 2 });
assert.equal(interactive.limit, 8);
assert.equal(batch.limit, 2);
assert.notEqual(interactive, batch);
</code></pre>
<p>Review rule: bulkhead modules must fail closed on local saturation, never steal slots across keys, and must not relabel rejection as a global NHTSA outage.</p>
<h2>Takeaway</h2>
<p>Bulkheading VIN decode workers keeps one bad path from starving the shared pool. Cap concurrency per tenant and route, fail or wait locally when a bulkhead is full, and keep that signal separate from breakers, hedges, and deadlines. Your free VIN proxy stays fair when interactive decodes cannot be crowded out by a single noisy batch or retry storm -- and when rejection copy admits local capacity, not invented upstream failure.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/bulkheading-vin-decode-workers-so-one-bad-vin-path-cannot-starve-the-pool-21gf">https://dev.to/vin_lookup_8dbd4710f77e9e/bulkheading-vin-decode-workers-so-one-bad-vin-path-cannot-starve-the-pool-21gf</a></p>
]]></content:encoded></item><item><title><![CDATA[Cache-Aside VIN Decodes So Misses Hit NHTSA Once and Hits Stay Honest About Age]]></title><description><![CDATA[A free VIN decode edge that always calls NHTSA wastes quota; one that serves cache forever lies about freshness. Stale-while-revalidate (covered elsewhere) serves soft-stale bodies while refreshing. E]]></description><link>https://freevinlookup.hashnode.dev/cache-aside-vin-decodes-so-misses-hit-nhtsa-once-and-hits-stay-honest-about-age</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/cache-aside-vin-decodes-so-misses-hit-nhtsa-once-and-hits-stay-honest-about-age</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 15:42:49 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode edge that always calls NHTSA wastes quota; one that serves cache forever lies about freshness. Stale-while-revalidate (covered elsewhere) serves soft-stale bodies while refreshing. ETag caching (elsewhere) skips unchanged representations. Negative cache (elsewhere) remembers invalid VINs. Stampede control and warmup (elsewhere) manage herds and prefetch. This post is different: classic <strong>cache-aside (lazy loading)</strong> -- on miss, fetch DecodeVinValues once, populate the cache, return the body; on hit, return the cached body with an honest age/freshness label; never invent specs on miss.</p>
<p>The goal is narrow: implement get-or-load for normalized VINs, single-populate on miss, expose cache age to the UI, and keep misses from becoming folklore cards. Cache-aside is deliberately boring infrastructure -- its value is correct loading and honest labeling, not clever speculation about what a VIN "should" contain.</p>
<h2>Cache-aside vs SWR, ETag, negative, stampede, warmup</h2>
<ul>
<li><strong>SWR</strong> -- serve soft-stale while revalidating in background</li>
<li><strong>ETag</strong> -- conditional revalidation with validators</li>
<li><strong>Negative cache</strong> -- remember failures / invalid VINs</li>
<li><strong>Stampede control</strong> -- one refresher when many hit expiry together</li>
<li><strong>Warmup</strong> -- populate before first user miss</li>
<li><strong>Cache-aside</strong> -- read path loads on miss; writer is the read path (or explicit put after live success)</li>
</ul>
<p>Cache-aside is the default lazy pattern. Pair it with stampede control when popular keys expire; do not confuse a simple miss-fill with SWR's soft-stale serve. On a cold miss you wait for live; on a warm hit you must still admit the body's age.</p>
<h2>Get-or-load with honest age</h2>
<pre><code class="language-ts">export type CacheAsideEntry = {
  vinNormalized: string;
  body: string;
  storedAt: number;
  maxAgeMs: number;
};

export type CacheAsideStore = Map&lt;string, CacheAsideEntry&gt;;

export type CacheAsideResult = {
  body: string;
  hit: boolean;
  ageMs: number;
  source: "cache" | "live";
};

export type LiveDecode = (vin: string) =&gt; Promise&lt;string&gt;;

export function getAside(
  store: CacheAsideStore,
  vinNormalized: string,
  now = Date.now(),
): CacheAsideEntry | null {
  const e = store.get(vinNormalized);
  if (!e) return null;
  if (now - e.storedAt &gt; e.maxAgeMs) {
    store.delete(vinNormalized);
    return null;
  }
  return e;
}

export function putAside(
  store: CacheAsideStore,
  vinNormalized: string,
  body: string,
  maxAgeMs = 86_400_000,
  now = Date.now(),
): CacheAsideEntry {
  const e: CacheAsideEntry = { vinNormalized, body, storedAt: now, maxAgeMs };
  store.set(vinNormalized, e);
  return e;
}

export async function getOrLoad(
  store: CacheAsideStore,
  vinNormalized: string,
  live: LiveDecode,
  maxAgeMs = 86_400_000,
  now = Date.now(),
): Promise&lt;CacheAsideResult&gt; {
  const hit = getAside(store, vinNormalized, now);
  if (hit) {
    return {
      body: hit.body,
      hit: true,
      ageMs: now - hit.storedAt,
      source: "cache",
    };
  }
  const body = await live(vinNormalized);
  putAside(store, vinNormalized, body, maxAgeMs, Date.now());
  return { body, hit: false, ageMs: 0, source: "live" };
}

export function freshnessLabel(r: CacheAsideResult): string {
  if (r.source === "live") {
    return "Decoded from live NHTSA (cache miss filled)";
  }
  const ageSec = Math.round(r.ageMs / 1000);
  return `Cache hit -- catalog body age ~${ageSec}s (not inventing newer specs)`;
}
</code></pre>
<p>On live failure, do not put a folklore body. Leave the key missing so the next call retries, or use negative cache (separate tool) for known-invalid VINs. If many clients miss the same key together, wrap <code>getOrLoad</code>'s live call in singleflight or stampede leadership so you still count one NHTSA fill -- cache-aside defines <em>where</em> the body lives; those tools define <em>who</em> may load it.</p>
<h2>Honesty rules for hits and misses</h2>
<p>Prefer:</p>
<ol>
<li><strong>One live fill per miss</strong> (add singleflight/stampede if many waiters share the miss)</li>
<li><strong>Age labels on hits</strong> so UI does not say "live just now"</li>
<li><strong>TTL expiry deletes or treats as miss</strong> -- no eternal silent hits</li>
<li><strong>No write of partial invented fields</strong> when live returns sparse Results (partial payload honesty elsewhere)</li>
</ol>
<p>Avoid serving a hit while claiming live completion, and avoid miss paths that invent Make from WMI charts. Emit metrics for hit ratio, miss latency, and average age-at-hit so you can tune <code>maxAgeMs</code> from data instead of folklore about how often vPIC changes.</p>
<h2>Forbidden upgrades</h2>
<ol>
<li>Populating cache with guessed catalog JSON when NHTSA fails</li>
<li>Labeling every hit as "live NHTSA just now"</li>
<li>Skipping TTL because "VIN decode never changes" (patterns and fields do evolve)</li>
<li>Using cache-aside put to store negative failures as successful empty cars</li>
<li>Warming by writing folklore for popular makes without a live body</li>
</ol>
<p>Refuse those. Cache-aside speeds repeats; it does not mint specs.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const store: CacheAsideStore = new Map();
let liveCalls = 0;
const live: LiveDecode = async () =&gt; {
  liveCalls += 1;
  return JSON.stringify({ Results: [{ Make: "HONDA" }] });
};

const a = await getOrLoad(store, "1HGCM82633A004352", live);
assert.equal(a.hit, false);
assert.equal(a.source, "live");
assert.equal(liveCalls, 1);

const b = await getOrLoad(store, "1HGCM82633A004352", live);
assert.equal(b.hit, true);
assert.equal(b.source, "cache");
assert.equal(liveCalls, 1);
assert.ok(/Cache hit/i.test(freshnessLabel(b)));
assert.ok(!/live NHTSA just now/i.test(freshnessLabel(b)));

// expired entry becomes miss
const e = store.get("1HGCM82633A004352")!;
e.storedAt = Date.now() - e.maxAgeMs - 1;
const c = await getOrLoad(store, "1HGCM82633A004352", live);
assert.equal(c.hit, false);
assert.equal(liveCalls, 2);
</code></pre>
<p>Review rule: cache-aside modules must fill from live on miss, label hit age honestly, and must not invent bodies on upstream failure.</p>
<h2>Takeaway</h2>
<p>Cache-aside VIN decodes hit NHTSA once per miss, store the real body, and serve repeats with an honest age label. Leave SWR, ETag, negative cache, stampede control, and warmup for their own jobs. Your free VIN edge stays fast when the cache is a shelf for sourced payloads -- never a factory for folklore catalog cards. Speed without age labels is how cached data pretends to be live.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>DEV article: forthcoming</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/cache-aside-vin-decodes-so-misses-hit-nhtsa-once-and-hits-stay-honest-about-age-57p1">https://dev.to/vin_lookup_8dbd4710f77e9e/cache-aside-vin-decodes-so-misses-hit-nhtsa-once-and-hits-stay-honest-about-age-57p1</a></p>
]]></content:encoded></item><item><title><![CDATA[Preventing VIN Decode Cache Stampedes When Soft TTL Expires Across Many Tabs]]></title><description><![CDATA[A free VIN decode that serves soft-stale bodies (SWR, covered elsewhere) still herds: when soft TTL expires for a popular VIN, many tabs each start live NHTSA DecodeVinValues. Singleflight (elsewhere)]]></description><link>https://freevinlookup.hashnode.dev/preventing-vin-decode-cache-stampedes-when-soft-ttl-expires-across-many-tabs</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/preventing-vin-decode-cache-stampedes-when-soft-ttl-expires-across-many-tabs</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 15:26:24 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode that serves soft-stale bodies (SWR, covered elsewhere) still herds: when soft TTL expires for a popular VIN, many tabs each start live NHTSA <code>DecodeVinValues</code>. Singleflight (elsewhere) merges in-flight promises in one process; warmup and negative caches handle other paths. This post is different: <strong>cache stampede control</strong> at soft-TTL expiry -- one revalidate wins, others keep serving stale, and jitter spreads expiry so popular keys do not wake together.</p>
<p>The goal: stop a thundering herd when soft TTL ends across tabs or workers, without inventing specs mid-herd, and without claiming a stampede-safe serve is "live just now."</p>
<h2>Stampede vs SWR, singleflight, warmup, negative cache</h2>
<ul>
<li><strong>SWR</strong> -- when a soft-stale body may be served while refreshing</li>
<li><strong>Singleflight</strong> -- merge identical in-flight promises once refresh starts</li>
<li><strong>Warmup / negative cache</strong> -- preload popular keys; skip known-bad VINs</li>
<li><strong>Stampede control</strong> -- <em>who</em> may start a refresh when many clients hit soft expiry, plus jitter so TTLs do not align</li>
</ul>
<p>Stampede without singleflight still doubles live calls across workers; SWR without stampede control invites a herd the moment soft TTL flips.</p>
<h2>Detect the herd window</h2>
<p>When soft TTL has expired but hard TTL has not, every concurrent reader wants a refresh. Without a leader lock, each calls live NHTSA.</p>
<pre><code class="language-ts">export type StampedeEntry = {
  vinNormalized: string;
  body: string;
  storedAt: number;
  softTtlMs: number;
  hardTtlMs: number;
  refreshLockUntil?: number; // epoch ms; leader holds refresh
  inflight?: Promise&lt;string&gt;;
};

export type StampedeStore = Map&lt;string, StampedeEntry&gt;;

export function softExpired(e: StampedeEntry, now = Date.now()): boolean {
  return now - e.storedAt &gt; e.softTtlMs;
}

export function hardExpired(e: StampedeEntry, now = Date.now()): boolean {
  return now - e.storedAt &gt; e.hardTtlMs;
}

/** Jitter soft TTL so popular VINs do not expire in lockstep. */
export function softTtlWithJitter(
  baseMs: number,
  vinNormalized: string,
  jitterFrac = 0.1,
): number {
  let h = 0;
  for (let i = 0; i &lt; vinNormalized.length; i++) {
    h = (Math.imul(31, h) + vinNormalized.charCodeAt(i)) | 0;
  }
  const unit = ((h &gt;&gt;&gt; 0) % 1000) / 1000; // 0..1
  const delta = (unit * 2 - 1) * jitterFrac; // -jitter..+jitter
  return Math.floor(baseMs * (1 + delta));
}
</code></pre>
<p>Jitter alone does not stop a herd after expiry -- it only desynchronizes <em>when</em> soft expiry hits. You still need a refresh leader.</p>
<h2>One leader revalidates; others serve stale</h2>
<p>On soft-stale hit: try to acquire a short refresh lock. Winner starts one live decode (optionally via singleflight). Losers return the stale body with an honest <code>revalidating</code> / <code>stale</code> label. Never invent Make/Model/Year while waiting.</p>
<pre><code class="language-ts">export type LiveDecode = (vin: string) =&gt; Promise&lt;string&gt;;

export type StampedeResult = {
  body: string;
  freshness: "fresh" | "stale" | "revalidating";
  source: "cache" | "live";
  leader: boolean;
};

const LOCK_MS = 5_000;

export async function getAvoidingStampede(
  store: StampedeStore,
  vinNormalized: string,
  live: LiveDecode,
  now = Date.now(),
): Promise&lt;StampedeResult&gt; {
  let entry = store.get(vinNormalized);
  if (!entry || hardExpired(entry, now)) {
    const body = await live(vinNormalized);
    const softTtlMs = softTtlWithJitter(3_600_000, vinNormalized);
    store.set(vinNormalized, {
      vinNormalized,
      body,
      storedAt: now,
      softTtlMs,
      hardTtlMs: 86_400_000,
    });
    return { body, freshness: "fresh", source: "live", leader: true };
  }

  if (!softExpired(entry, now)) {
    return {
      body: entry.body,
      freshness: "fresh",
      source: "cache",
      leader: false,
    };
  }

  // Soft-stale: elect a leader for refresh
  const lockHeld =
    entry.refreshLockUntil !== undefined &amp;&amp; entry.refreshLockUntil &gt; now;
  if (!lockHeld &amp;&amp; !entry.inflight) {
    entry.refreshLockUntil = now + LOCK_MS;
    entry.inflight = live(vinNormalized)
      .then((body) =&gt; {
        store.set(vinNormalized, {
          vinNormalized,
          body,
          storedAt: Date.now(),
          softTtlMs: softTtlWithJitter(3_600_000, vinNormalized),
          hardTtlMs: 86_400_000,
        });
        return body;
      })
      .finally(() =&gt; {
        const cur = store.get(vinNormalized);
        if (cur) {
          delete cur.inflight;
          delete cur.refreshLockUntil;
        }
      });
    return {
      body: entry.body,
      freshness: "revalidating",
      source: "cache",
      leader: true,
    };
  }

  // Followers: serve stale, do not start another live call
  return {
    body: entry.body,
    freshness: entry.inflight ? "revalidating" : "stale",
    source: "cache",
    leader: false,
  };
}

export function stampedeFootnote(r: StampedeResult): string {
  if (r.freshness === "fresh" &amp;&amp; r.source === "live") {
    return "Decoded from live NHTSA for this request";
  }
  if (r.freshness === "revalidating") {
    return r.leader
      ? "Serving soft-stale body; this tab leads refresh"
      : "Serving soft-stale body; another tab leads refresh";
  }
  return "Serving soft-stale catalog body; soft TTL expired";
}
</code></pre>
<p>Across processes, replace the in-memory lock with Redis <code>SET NX</code> or a similar lease. Honesty stays: followers never invent a card; leaders never label a pre-refresh stale body as live.</p>
<h2>Forbidden upgrades</h2>
<ol>
<li>Letting every soft-expired tab call NHTSA "just to be sure"</li>
<li>Extending hard TTL forever to dodge stampedes (hides staleness forever)</li>
<li>Filling missing fields during the herd with Make/Model/Year folklore</li>
<li>Labeling follower stale serves as "live NHTSA just now"</li>
<li>Dropping the stale body and returning an empty spinner for every follower</li>
</ol>
<p>Refuse those. Stampede control is about upstream calm and honest freshness -- not fake completeness.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const store: StampedeStore = new Map();
const vin = "1HGCM82633A004352";
let liveCalls = 0;
const live: LiveDecode = async () =&gt; {
  liveCalls += 1;
  await new Promise((r) =&gt; setTimeout(r, 20));
  return JSON.stringify({ Results: [{ Make: "HONDA" }] });
};

const t0 = 1_000;
const first = await getAvoidingStampede(store, vin, live, t0);
assert.equal(first.source, "live");
assert.equal(liveCalls, 1);

const softAt = t0 + store.get(vin)!.softTtlMs + 1;
const a = getAvoidingStampede(store, vin, live, softAt);
const b = getAvoidingStampede(store, vin, live, softAt);
const [ra, rb] = await Promise.all([a, b]);
assert.equal(ra.source, "cache");
assert.equal(rb.source, "cache");
assert.ok(ra.leader !== rb.leader); // exactly one leader
assert.equal(liveCalls, 2); // miss + one revalidate, not three
assert.ok(!/live NHTSA just now/i.test(stampedeFootnote(rb)));
await store.get(vin)?.inflight;
</code></pre>
<p>Review rule: stampede modules must elect one refresh leader per key and must not invent specs for followers.</p>
<h2>Takeaway</h2>
<p>When soft TTL expires across many tabs, a VIN decode cache can stampede NHTSA unless one leader revalidates and followers keep serving soft-stale bodies with honest labels. Jitter desynchronizes expiry; locks or singleflight collapse the herd; hard TTL still forces a live wait when the body is too old. Stampede control beside SWR stays fast and truthful -- without pretending every stale serve was a fresh live decode.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>DEV article: forthcoming</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/preventing-vin-decode-cache-stampedes-when-soft-ttl-expires-across-many-tabs-56l1">https://dev.to/vin_lookup_8dbd4710f77e9e/preventing-vin-decode-cache-stampedes-when-soft-ttl-expires-across-many-tabs-56l1</a></p>
]]></content:encoded></item><item><title><![CDATA[Singleflight VIN Decode Lookups So Concurrent Identical VINs Share One In-Flight Promise]]></title><description><![CDATA[A free VIN decode edge or API route often fans out to NHTSA DecodeVinValues under concurrency. Two serverless isolates, two queue workers, or two browser tabs can hit the same normalized VIN in the sa]]></description><link>https://freevinlookup.hashnode.dev/singleflight-vin-decode-lookups-so-concurrent-identical-vins-share-one-in-flight-promise</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/singleflight-vin-decode-lookups-so-concurrent-identical-vins-share-one-in-flight-promise</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 15:09:03 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode edge or API route often fans out to NHTSA DecodeVinValues under concurrency. Two serverless isolates, two queue workers, or two browser tabs can hit the same normalized VIN in the same millisecond. Without a server-side singleflight gate, each request opens its own upstream call even when the work is identical and already in flight on that process.</p>
<p>This post is about <strong>server/edge singleflight</strong>: a process-local map from normalized VIN to one in-flight promise so concurrent identical lookups share one DecodeVinValues. Client-side request coalesce (duplicate pastes in one browser session) is a related idea with a different boundary -- see that pattern separately. Debounce decides when typing may start a call. ETag caches skip repeats after success. Here the focus is sharing one in-flight promise across concurrent <em>server</em> callers for the same VIN.</p>
<h2>The failure mode</h2>
<p>Typical sequence without singleflight on the edge:</p>
<ol>
<li>Request A arrives for VIN X; the handler starts DecodeVinValues.</li>
<li>Request B arrives for the same normalized X before A settles (second tab, prefetch, or parallel SSR).</li>
<li>The handler starts a second identical upstream fetch.</li>
<li>NHTSA sees two calls; your edge latency and quota budget take a hit for no new information.</li>
<li>Both responses return; you still paid twice for one pattern.</li>
</ol>
<p>Client coalesce does not fix this: the duplicates already crossed the network as separate HTTP requests. A response cache helps after success, not while the first edge fetch is still pending. A global mutex that serializes <em>all</em> VINs is worse -- unrelated lookups wait on each other.</p>
<h2>One promise per normalized VIN on the process</h2>
<p>Own a module-scoped map from normalized VIN to the in-flight <code>Promise</code>. On decode:</p>
<ol>
<li>Normalize and validate the VIN.</li>
<li>If the map already has an entry for that key, return it.</li>
<li>Otherwise create the upstream promise, store it, and <code>finally</code> delete the key when settled.</li>
</ol>
<p>Concurrent waiters on the same isolate share the result. Distinct VINs still run in parallel -- singleflight is keyed by identity, not a process-wide lock. Multi-isolate cold starts still duplicate until you add a shared cache; singleflight only collapses concurrency <em>inside</em> one runtime.</p>
<pre><code class="language-ts">const VIN_RE = /^[A-HJ-NPR-Z0-9]{17}$/;

export function normalizeVin(raw: string): string {
  return raw.trim().toUpperCase().replace(/[\s\-._]/g, "");
}

export type DecodeRow = Record&lt;string, string&gt;;

export type SingleflightMap = Map&lt;string, Promise&lt;DecodeRow&gt;&gt;;

export function singleflightDecode(
  inflight: SingleflightMap,
  rawVin: string,
  fetchDecode: (vin: string) =&gt; Promise&lt;DecodeRow&gt;,
): Promise&lt;DecodeRow&gt; {
  const vin = normalizeVin(rawVin);
  if (!VIN_RE.test(vin)) {
    return Promise.reject(new Error("invalid_vin"));
  }

  const existing = inflight.get(vin);
  if (existing) return existing;

  const pending = fetchDecode(vin).finally(() =&gt; {
    if (inflight.get(vin) === pending) inflight.delete(vin);
  });
  inflight.set(vin, pending);
  return pending;
}
</code></pre>
<p>The <code>finally</code> cleanup matters. Leaving settled promises in the map would pin memory and block honest retries after a failed attempt. Delete only when the map still points at <em>this</em> promise so a concurrent re-entry after failure can start fresh work.</p>
<h2>Forbidden shortcuts</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>A process-wide lock so every decode waits on whichever VIN started first</li>
<li>Treating singleflight as a substitute for a shared Redis/ETag cache across isolates</li>
<li>Claiming "instant decode -- no NHTSA" when you only shared one in-flight edge call</li>
<li>Keeping failed 5xx promises in the map so retries never leave</li>
<li>Keying by raw query string without normalize (case or hyphens create duplicate flights)</li>
</ol>
<p>Refuse those. Singleflight identical <em>valid</em> in-flight work on one process. Keep client coalesce for same-session duplicate pastes, debounce for typing, and durable caches for post-success repeats across instances. Do not invent offline or "skipped upstream" marketing from a shared promise.</p>
<pre><code class="language-ts">export function assertNoSingleflightAbuse(moduleSource: string): void {
  const banned = [
    /global.?lock.?all.?vins/i,
    /skip.?nhtsa.?via.?singleflight/i,
    /instant.?decode.?no.?upstream/i,
    /singleflight.?replaces.?cache/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(`Singleflight module misuse: ${re}`);
    }
  }
}

export async function decodeWithSingleflight(
  inflight: SingleflightMap,
  rawVin: string,
  fetchDecode: (vin: string) =&gt; Promise&lt;DecodeRow&gt;,
  onShared: () =&gt; void,
): Promise&lt;DecodeRow&gt; {
  const vin = normalizeVin(rawVin);
  const wasShared = inflight.has(vin);
  const row = await singleflightDecode(inflight, rawVin, fetchDecode);
  if (wasShared) onShared();
  return row;
}
</code></pre>
<p>Instrument <code>onShared</code> (or a metric) so you can see how often concurrent edge callers joined. That is observability, not a claim that decode is free or that NHTSA was skipped.</p>
<h2>API copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>Silent share: concurrent waiters see the same loading outcome without a second upstream span</li>
<li>Optional quiet log or metric: "joined in-flight singleflight for this VIN"</li>
<li>Clear error if the shared promise rejects -- every waiter sees the same failure</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"Cached instantly -- no NHTSA call" when you only singleflighted an open edge request</li>
<li>Hiding a shared failure from secondary waiters</li>
<li>Showing success for VIN B because A's in-flight promise was incorrectly keyed</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const inflight: SingleflightMap = new Map();
let calls = 0;
const fetchDecode = async (vin: string): Promise&lt;DecodeRow&gt; =&gt; {
  calls += 1;
  await new Promise((r) =&gt; setTimeout(r, 20));
  return { VIN: vin, Make: "TEST" };
};

const p1 = singleflightDecode(inflight, "1HGCM82633A004352", fetchDecode);
const p2 = singleflightDecode(inflight, "1hgcm82633a004352", fetchDecode);
assert.equal(p1, p2);
const [a, b] = await Promise.all([p1, p2]);
assert.equal(a.Make, "TEST");
assert.equal(b.Make, "TEST");
assert.equal(calls, 1);
assert.equal(inflight.size, 0);

const p3 = singleflightDecode(inflight, "1HGCM82633A004352", fetchDecode);
await p3;
assert.equal(calls, 2);
</code></pre>
<p>Review rule: singleflight modules must share by normalized VIN only while pending on this process, clear on settle, and never claim to replace client coalesce, debounce, or durable response caches.</p>
<h2>Takeaway</h2>
<p>Server/edge singleflight collapses concurrent identical VIN lookups into one upstream DecodeVinValues call per process. Pair it with client coalesce (same-session duplicates), debounce (start timing), and ETag caches (post-success repeats) -- do not substitute one for another. Your free VIN API stays kind to NHTSA when mid-flight work on one runtime shares a promise, then clears so the next lookup can run honestly.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/singleflight-vin-decode-lookups-so-concurrent-identical-vins-share-one-in-flight-promise-29nn">https://dev.to/vin_lookup_8dbd4710f77e9e/singleflight-vin-decode-lookups-so-concurrent-identical-vins-share-one-in-flight-promise-29nn</a></p>
]]></content:encoded></item><item><title><![CDATA[Limiting Concurrent VIN Decode Upstream Calls So Browser Tabs Do Not Exhaust NHTSA]]></title><description><![CDATA[A free VIN decode edge that forwards every browser tab's paste straight to live NHTSA DecodeVinValues can open dozens of simultaneous upstream sockets. Token buckets (covered elsewhere) shape request ]]></description><link>https://freevinlookup.hashnode.dev/limiting-concurrent-vin-decode-upstream-calls-so-browser-tabs-do-not-exhaust-nhtsa</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/limiting-concurrent-vin-decode-upstream-calls-so-browser-tabs-do-not-exhaust-nhtsa</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 15:06:45 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode edge that forwards every browser tab's paste straight to live NHTSA <code>DecodeVinValues</code> can open dozens of simultaneous upstream sockets. Token buckets (covered elsewhere) shape request <em>rate</em> over time. Quota-header handlers (elsewhere) back off when NHTSA says so. Singleflight (elsewhere) merges identical in-flight VINs. Stampede control (elsewhere) elects one soft-TTL refresher. This post is different: a <strong>concurrency limit</strong> -- a semaphore / max-in-flight pool so only N DecodeVinValues calls run at once, with clear queue-wait vs reject behavior and no invented specs while waiting.</p>
<p>The goal is narrow: cap simultaneous upstream calls from your edge (or BFF), queue excess work briefly or reject with an honest busy signal, and never fabricate a decode card to "keep the UI full" under load.</p>
<h2>Concurrency limit vs rate limit, singleflight, stampede</h2>
<ul>
<li><strong>Token bucket / rate limit</strong> -- how many starts per second over a window</li>
<li><strong>Quota headers</strong> -- honor upstream Retry-After / remaining when told</li>
<li><strong>Singleflight</strong> -- one shared promise per identical VIN key</li>
<li><strong>Stampede control</strong> -- one soft-TTL refresh leader per key</li>
<li><strong>Concurrency limit</strong> -- how many live upstream calls may be <em>in flight at once</em>, regardless of VIN identity</li>
</ul>
<p>A rate limit of 10/s can still open 50 sockets if each call takes 5s. A concurrency cap of 8 keeps peak sockets honest even when many distinct VINs arrive together.</p>
<h2>Semaphore shape</h2>
<p>Track <code>inFlight</code> and a FIFO wait queue. Acquire before calling live NHTSA; release in <code>finally</code>. Bound queue length and wait time so tabs do not hang forever.</p>
<pre><code class="language-ts">export type LiveDecode = (vin: string) =&gt; Promise&lt;string&gt;;

export type LimitResult =
  | { ok: true; body: string; waitedMs: number }
  | { ok: false; reason: "queue-full" | "wait-timeout"; waitedMs: number };

export type ConcurrencyLimit = {
  maxInFlight: number;
  maxQueue: number;
  maxWaitMs: number;
  inFlight: number;
  waiters: Array&lt;{
    enqueuedAt: number;
    resolve: (granted: boolean) =&gt; void;
    timer: ReturnType&lt;typeof setTimeout&gt;;
  }&gt;;
};

export function createLimit(
  maxInFlight = 8,
  maxQueue = 32,
  maxWaitMs = 3_000,
): ConcurrencyLimit {
  return { maxInFlight, maxQueue, maxWaitMs, inFlight: 0, waiters: [] };
}

function grant(limit: ConcurrencyLimit): void {
  limit.inFlight += 1;
}

function release(limit: ConcurrencyLimit): void {
  limit.inFlight -= 1;
  const next = limit.waiters.shift();
  if (!next) return;
  clearTimeout(next.timer);
  next.resolve(true);
}

async function acquire(
  limit: ConcurrencyLimit,
  now = Date.now(),
): Promise&lt;{ granted: boolean; waitedMs: number }&gt; {
  if (limit.inFlight &lt; limit.maxInFlight) {
    grant(limit);
    return { granted: true, waitedMs: 0 };
  }
  if (limit.waiters.length &gt;= limit.maxQueue) {
    return { granted: false, waitedMs: 0 };
  }
  const enqueuedAt = now;
  const granted = await new Promise&lt;boolean&gt;((resolve) =&gt; {
    const timer = setTimeout(() =&gt; {
      const idx = limit.waiters.findIndex((w) =&gt; w.resolve === resolve);
      if (idx &gt;= 0) limit.waiters.splice(idx, 1);
      resolve(false);
    }, limit.maxWaitMs);
    limit.waiters.push({ enqueuedAt, resolve, timer });
  });
  const waitedMs = Date.now() - enqueuedAt;
  if (!granted) return { granted: false, waitedMs };
  grant(limit);
  return { granted: true, waitedMs };
}

export async function decodeWithLimit(
  limit: ConcurrencyLimit,
  vinNormalized: string,
  live: LiveDecode,
): Promise&lt;LimitResult&gt; {
  const { granted, waitedMs } = await acquire(limit);
  if (!granted) {
    return {
      ok: false,
      reason: waitedMs &gt; 0 ? "wait-timeout" : "queue-full",
      waitedMs,
    };
  }
  try {
    const body = await live(vinNormalized);
    return { ok: true, body, waitedMs };
  } finally {
    release(limit);
  }
}

export function busyMessage(r: Extract&lt;LimitResult, { ok: false }&gt;): string {
  return r.reason === "queue-full"
    ? "Decode busy: upstream concurrency queue full -- try again shortly"
    : "Decode busy: waited too long for an upstream slot -- try again shortly";
}
</code></pre>
<p>Never map a reject into a fake Make/Model/Year card. Show the busy message; optionally retry client-side with backoff after the user confirms.</p>
<h2>Queue wait vs reject</h2>
<p>Product pressure often wants infinite queues so "nobody sees an error." Prefer bounded wait:</p>
<ol>
<li><strong>Short queue + timeout</strong> -- absorb bursts, then reject honestly</li>
<li><strong>Reject fast when full</strong> -- protects NHTSA and your edge memory</li>
<li><strong>Do not invent specs</strong> while a tab waits for a slot</li>
<li><strong>Combine with singleflight</strong> after acquire so identical VINs still share one live body</li>
<li><strong>Combine with token bucket</strong> so even under the concurrency cap you do not exceed sustained rate</li>
</ol>
<p>Concurrency limits are about <em>peak sockets</em>; rate limits are about <em>sustained starts</em>. Use both.</p>
<h2>Forbidden upgrades</h2>
<ol>
<li>Raising <code>maxInFlight</code> to "unlimited" under load so every tab opens a socket</li>
<li>Returning folklore catalog rows when <code>queue-full</code> or <code>wait-timeout</code> fires</li>
<li>Labeling a queued wait as "live NHTSA completed" before acquire succeeds</li>
<li>Dropping the semaphore because singleflight "already dedupes" (it does not cap distinct VINs)</li>
<li>Ignoring NHTSA quota headers because the concurrency cap "should be enough"</li>
</ol>
<p>Refuse those. An honest busy state beats a stampeded upstream and a lying card.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const limit = createLimit(2, 2, 50);
let liveCalls = 0;
const live: LiveDecode = async (vin) =&gt; {
  liveCalls += 1;
  await new Promise((r) =&gt; setTimeout(r, 80));
  return JSON.stringify({ Results: [{ VIN: vin }] });
};

const jobs = ["VINAAAA", "VINBBBB", "VINCCCC", "VINDDDD", "VINEEEE"].map(
  (v) =&gt; decodeWithLimit(limit, v, live),
);
const results = await Promise.all(jobs);
const oks = results.filter((r) =&gt; r.ok);
const fails = results.filter((r) =&gt; !r.ok);
assert.ok(oks.length &gt;= 2);
assert.ok(fails.length &gt;= 1);
assert.ok(liveCalls &lt;= 4); // cap + short queue, not five parallel forever
assert.ok(
  fails.every((f) =&gt; !f.ok &amp;&amp; /busy|queue|waited/i.test(busyMessage(f))),
);
assert.ok(
  !results.some(
    (r) =&gt; r.ok &amp;&amp; /folklore|invented/i.test(r.body),
  ),
);
</code></pre>
<p>Review rule: concurrency modules must bound in-flight upstream calls and must not invent decode bodies on reject.</p>
<h2>Takeaway</h2>
<p>A concurrency limit keeps browser tabs from opening unbounded DecodeVinValues sockets against NHTSA. Cap in-flight calls, bound the wait queue, reject with an honest busy signal, and leave rate limits, quota headers, singleflight, and stampede control in their own lanes. Your free VIN edge stays calm when peak concurrency is explicit -- and never fills a busy moment with invented catalog marketing.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>DEV article: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/limiting-concurrent-vin-decode-upstream-calls-so-browser-tabs-do-not-exhaust-nhtsa-46a3">https://dev.to/vin_lookup_8dbd4710f77e9e/limiting-concurrent-vin-decode-upstream-calls-so-browser-tabs-do-not-exhaust-nhtsa-46a3</a></p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/limiting-concurrent-vin-decode-upstream-calls-so-browser-tabs-do-not-exhaust-nhtsa-46a3">https://dev.to/vin_lookup_8dbd4710f77e9e/limiting-concurrent-vin-decode-upstream-calls-so-browser-tabs-do-not-exhaust-nhtsa-46a3</a></p>
]]></content:encoded></item><item><title><![CDATA[Serving Stale-While-Revalidate VIN Decodes So Repeat Lookups Stay Fast Without Lying About Freshness]]></title><description><![CDATA[A free VIN decode that always waits for live NHTSA DecodeVinValues on every repeat paste feels slow even when the catalog barely changes. ETag caches, negative caches, and warmup (covered elsewhere) s]]></description><link>https://freevinlookup.hashnode.dev/serving-stale-while-revalidate-vin-decodes-so-repeat-lookups-stay-fast-without-lying-about-freshness</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/serving-stale-while-revalidate-vin-decodes-so-repeat-lookups-stay-fast-without-lying-about-freshness</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:49:08 GMT</pubDate><content:encoded><![CDATA[<p>A free VIN decode that always waits for live NHTSA <code>DecodeVinValues</code> on every repeat paste feels slow even when the catalog barely changes. ETag caches, negative caches, and warmup (covered elsewhere) solve other problems. This post is different: <strong>stale-while-revalidate (SWR)</strong> -- serve a slightly stale cached decode immediately, refresh in the background, and label freshness honestly so the UI never pretends a background refresh already finished.</p>
<p>The goal is narrow: return a cached body past soft TTL while a revalidate runs, expose <code>fresh</code> vs <code>stale</code> vs <code>revalidating</code> to callers, and never invent specs or claim "live NHTSA just now" when you served stale.</p>
<h2>SWR vs ETag, negative cache, and warmup</h2>
<p>Use SWR when:</p>
<ul>
<li>Soft TTL expired but hard TTL has not -- body may still be useful</li>
<li>A repeat lookup should paint fast while a background DecodeVinValues runs</li>
<li>Callers can show "catalog snapshot; refreshing..." without blocking</li>
</ul>
<p>Do <strong>not</strong> confuse SWR with:</p>
<ul>
<li>ETag / If-None-Match conditional fetches (validator equality, not soft-stale serve)</li>
<li>Negative cache of invalid VINs (no successful body to serve stale)</li>
<li>Make-pattern warmup (prefetch popular keys before first user hit)</li>
</ul>
<p>SWR is about <strong>freshness semantics on a successful cached body</strong>. When hard TTL expires, drop the entry and require a blocking live fetch. Never serve a negative entry as a successful card.</p>
<h2>Soft TTL, hard TTL, and honesty labels</h2>
<p>Store soft and hard windows. Soft expiry allows stale serve + background revalidate. Hard expiry forces a live wait. Label every response so the UI cannot lie.</p>
<pre><code class="language-ts">export type Freshness = "fresh" | "stale" | "revalidating" | "miss";

export type SwrEntry = {
  vinNormalized: string;
  body: string;
  storedAt: number; // epoch ms
  softTtlMs: number;
  hardTtlMs: number;
  revalidating?: Promise&lt;string&gt;;
};

export type SwrStore = Map&lt;string, SwrEntry&gt;;

export type SwrResult = {
  body: string | null;
  freshness: Freshness;
  source: "cache" | "live" | "missing";
};

const SOFT_TTL_MS = 60 * 60 * 1000; // 1h soft
const HARD_TTL_MS = 24 * 60 * 60 * 1000; // 24h hard

export function ageMs(entry: SwrEntry, now = Date.now()): number {
  return now - entry.storedAt;
}

export function classify(entry: SwrEntry, now = Date.now()): Freshness {
  const age = ageMs(entry, now);
  if (age &gt; entry.hardTtlMs) return "miss";
  if (age &lt;= entry.softTtlMs) return "fresh";
  if (entry.revalidating) return "revalidating";
  return "stale";
}
</code></pre>
<p>A <code>fresh</code> body may still be hours old relative to wall clock -- that is fine if soft TTL allows it. What is <strong>not</strong> fine is labeling a soft-stale serve as "live decode completed just now."</p>
<h2>Serve stale, revalidate once</h2>
<p>On soft-stale hit: return the cached body immediately, start one background revalidate (coalesce concurrent callers onto the same promise), and surface <code>revalidating</code> so the UI can footnote.</p>
<pre><code class="language-ts">export type LiveDecode = (vin: string) =&gt; Promise&lt;string&gt;;

export async function getWithSwr(
  store: SwrStore,
  vinNormalized: string,
  live: LiveDecode,
  now = Date.now(),
): Promise&lt;SwrResult&gt; {
  const entry = store.get(vinNormalized);
  if (!entry || ageMs(entry, now) &gt; entry.hardTtlMs) {
    const body = await live(vinNormalized);
    store.set(vinNormalized, {
      vinNormalized,
      body,
      storedAt: now,
      softTtlMs: SOFT_TTL_MS,
      hardTtlMs: HARD_TTL_MS,
    });
    return { body, freshness: "fresh", source: "live" };
  }

  const freshness = classify(entry, now);
  if (freshness === "fresh") {
    return { body: entry.body, freshness: "fresh", source: "cache" };
  }

  // Soft-stale: serve body, kick revalidate if not already running
  if (!entry.revalidating) {
    entry.revalidating = live(vinNormalized)
      .then((body) =&gt; {
        store.set(vinNormalized, {
          vinNormalized,
          body,
          storedAt: Date.now(),
          softTtlMs: SOFT_TTL_MS,
          hardTtlMs: HARD_TTL_MS,
        });
        return body;
      })
      .finally(() =&gt; {
        const cur = store.get(vinNormalized);
        if (cur) delete cur.revalidating;
      });
  }

  return {
    body: entry.body,
    freshness: "revalidating",
    source: "cache",
  };
}

export function freshnessFootnote(r: SwrResult): string {
  switch (r.freshness) {
    case "fresh":
      return r.source === "live"
        ? "Decoded from live NHTSA for this request"
        : "Cached catalog body within soft TTL";
    case "stale":
      return "Serving stale catalog body; soft TTL expired";
    case "revalidating":
      return "Serving stale catalog body while refreshing from NHTSA";
    case "miss":
      return "No usable cache entry; live decode required";
  }
}
</code></pre>
<p>Never rewrite the stale body while revalidate is in flight. Swap only when the live promise resolves. If revalidate fails, keep serving the prior body with an honest error footnote -- do not invent Make/Model/Year to "heal" the miss.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Labeling every SWR serve as "live NHTSA just now"</li>
<li>Extending hard TTL forever so stale bodies never expire</li>
<li>Serving negative-cache failures as successful decode cards</li>
<li>Inventing missing catalog fields while a revalidate is pending</li>
<li>Hiding the revalidating footnote so cards look always-fresh</li>
</ol>
<p>Refuse those. Speed without honesty is a lie about freshness.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const store: SwrStore = new Map();
const vin = "1HGCM82633A004352";
let liveCalls = 0;
const live: LiveDecode = async () =&gt; {
  liveCalls += 1;
  return JSON.stringify({ Results: [{ Make: "HONDA" }] });
};

const first = await getWithSwr(store, vin, live, 1_000);
assert.equal(first.freshness, "fresh");
assert.equal(first.source, "live");
assert.equal(liveCalls, 1);

const softStaleAt = 1_000 + SOFT_TTL_MS + 1;
const second = await getWithSwr(store, vin, live, softStaleAt);
assert.equal(second.source, "cache");
assert.equal(second.freshness, "revalidating");
assert.ok(/stale|refreshing/i.test(freshnessFootnote(second)));
assert.ok(!/live NHTSA just now/i.test(freshnessFootnote(second)));

// Concurrent soft-stale callers share one revalidate
const third = await getWithSwr(store, vin, live, softStaleAt);
assert.equal(third.freshness, "revalidating");
await store.get(vin)!.revalidating;
assert.equal(liveCalls, 2); // one live + one revalidate, not three
</code></pre>
<p>Review rule: SWR modules must expose freshness labels and must not claim live completion for stale serves.</p>
<h2>Takeaway</h2>
<p>Stale-while-revalidate keeps repeat VIN lookups fast by serving a soft-stale catalog body while NHTSA refreshes in the background. Pair soft and hard TTLs, coalesce revalidates, and label every response so users never confuse a stale snapshot with a just-completed live decode. Speed and honesty travel together; ETag, negative, and warmup caches stay in their own lanes.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/serving-stale-while-revalidate-vin-decodes-so-repeat-lookups-stay-fast-without-lying-about-freshness-1h99">https://dev.to/vin_lookup_8dbd4710f77e9e/serving-stale-while-revalidate-vin-decodes-so-repeat-lookups-stay-fast-without-lying-about-freshness-1h99</a></p>
]]></content:encoded></item><item><title><![CDATA[Client Idempotency Keys for VIN Decode Retries Without Double-Charging Upstream Quotas]]></title><description><![CDATA[Mobile networks drop mid-response. Users tap Decode twice. Your fetch layer retries a 504 with the same VIN. Each attempt that reaches NHTSA burns the same free-quota unit even when the user intent wa]]></description><link>https://freevinlookup.hashnode.dev/client-idempotency-keys-for-vin-decode-retries-without-double-charging-upstream-quotas</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/client-idempotency-keys-for-vin-decode-retries-without-double-charging-upstream-quotas</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:31:39 GMT</pubDate><content:encoded><![CDATA[<p>Mobile networks drop mid-response. Users tap Decode twice. Your fetch layer retries a 504 with the same VIN. Each attempt that reaches NHTSA burns the same free-quota unit even when the user intent was one decode. Handler-side idempotency stores (covered elsewhere) protect your API after the request arrives. This post is about the <strong>client-generated key</strong> that travels with every retry so an edge, BFF, or gateway can refuse a second upstream call before quota is spent.</p>
<p>The goal is narrow: one logical decode attempt, one client key, zero duplicate NHTSA hits when the network or the user repeats themselves. Do not confuse this with VIN-content caching ("same VIN forever") or with circuit breakers that open on slow upstreams.</p>
<h2>Why the client must mint the key</h2>
<p>Server-derived keys built only from normalized VIN + userId collide across <strong>different</strong> user intents: yesterday's decode and today's deliberate refresh share the same fingerprint. For quota protection on retries, you want the opposite of forever-dedupe: a short-lived key that is stable for one button press and its automatic retries, then expires so a later deliberate click is a new charge.</p>
<p>The browser (or mobile app) is the right place to mint that key:</p>
<ol>
<li>User taps Decode (or the form auto-submits once)</li>
<li>Client creates a UUID (or ULID) and stores it next to the in-flight VIN</li>
<li>Every retry of that attempt sends the <strong>same</strong> <code>Idempotency-Key</code> header</li>
<li>A fresh tap after success or cancel mints a <strong>new</strong> key</li>
</ol>
<p>That pattern preserves legitimate second decodes while collapsing double-clicks and transport retries onto one upstream budget.</p>
<h2>Shape the request, not the VIN</h2>
<pre><code class="language-ts">export type DecodeAttempt = {
  vinNormalized: string;
  idempotencyKey: string;
  createdAt: number;
};

const ATTEMPT_TTL_MS = 60_000;

export function mintDecodeAttempt(vinNormalized: string): DecodeAttempt {
  return {
    vinNormalized,
    idempotencyKey: crypto.randomUUID(),
    createdAt: Date.now(),
  };
}

export function headerForAttempt(attempt: DecodeAttempt): HeadersInit {
  return {
    "Idempotency-Key": attempt.idempotencyKey,
    "Content-Type": "application/json",
  };
}

export function isAttemptFresh(attempt: DecodeAttempt, now = Date.now()): boolean {
  return now - attempt.createdAt &lt; ATTEMPT_TTL_MS;
}
</code></pre>
<p>Send the key on POST (or on your BFF's decode route). Do <strong>not</strong> bake the key into the VIN string. Do <strong>not</strong> reuse yesterday's key from <code>localStorage</code> across sessions -- that silently merges unrelated user actions and can skip a decode the buyer intended.</p>
<h2>Edge / BFF: honor the key before calling NHTSA</h2>
<p>Your product layer still owns side effects (metrics, audit rows, UI history). The client key lets the edge short-circuit <strong>before</strong> <code>DecodeVinValues</code>:</p>
<pre><code class="language-ts">type GateResult =
  | { kind: "proceed" }
  | { kind: "replay"; body: unknown }
  | { kind: "inflight" };

type StoredAttempt = {
  status: "pending" | "complete";
  body?: unknown;
  expiresAt: number;
};

const attempts = new Map&lt;string, StoredAttempt&gt;(); // Redis in production

export function gateUpstream(
  key: string,
  ttlMs = 60_000,
): GateResult {
  const now = Date.now();
  const existing = attempts.get(key);
  if (existing &amp;&amp; existing.expiresAt &gt; now) {
    if (existing.status === "complete") {
      return { kind: "replay", body: existing.body };
    }
    return { kind: "inflight" };
  }
  attempts.set(key, { status: "pending", expiresAt: now + ttlMs });
  return { kind: "proceed" };
}

export function completeAttempt(key: string, body: unknown, ttlMs = 60_000): void {
  attempts.set(key, {
    status: "complete",
    body,
    expiresAt: Date.now() + ttlMs,
  });
}
</code></pre>
<p>On <code>proceed</code>, call NHTSA once, then <code>completeAttempt</code>. On <code>replay</code>, return the stored body without touching upstream. On <code>inflight</code>, return 409 or a typed "decode already in progress" so the client waits instead of opening a second socket.</p>
<h2>Client retry loop that keeps the key</h2>
<pre><code class="language-ts">export async function decodeWithClientKey(
  attempt: DecodeAttempt,
  post: (init: RequestInit) =&gt; Promise&lt;Response&gt;,
  maxRetries = 2,
): Promise&lt;Response&gt; {
  if (!isAttemptFresh(attempt)) {
    throw new Error("idempotency attempt expired; mint a new key");
  }
  let last: Response | undefined;
  for (let i = 0; i &lt;= maxRetries; i++) {
    last = await post({
      method: "POST",
      headers: headerForAttempt(attempt),
      body: JSON.stringify({ vin: attempt.vinNormalized }),
    });
    if (last.status === 409) {
      await new Promise((r) =&gt; setTimeout(r, 200 * (i + 1)));
      continue;
    }
    if (last.ok || last.status &lt; 500) return last;
  }
  return last!;
}
</code></pre>
<p>Critical rule: retries must <strong>not</strong> call <code>mintDecodeAttempt</code> again. Minting on each retry is how you double-charge quota while believing you are being careful.</p>
<h2>Quota accounting stays honest</h2>
<p>Count an upstream charge only when <code>DecodeVinValues</code> actually leaves your process. Replays from <code>gateUpstream</code> and 409-inflight waits must not increment NHTSA counters or "decodes today" meters. If the dashboard shows three upstream calls for one client key, the gate is wrong or the retry loop is minting keys. Keep server TTL short (30â€“90s) so deliberate refreshes stay real charges.</p>
<h2>Forbidden patterns</h2>
<ol>
<li>Deriving the only key from VIN alone (collides intentional refreshes; also skips cache-bust when you need one)</li>
<li>Minting a new UUID on every retry tick</li>
<li>Persisting keys forever in <code>localStorage</code> so next week's visit replays last week's body</li>
<li>Treating a client key as proof the VIN is valid -- still validate length and check digit locally</li>
<li>Counting local validation failures as upstream quota events</li>
</ol>
<p>Client keys protect <strong>transport and double-submit</strong> windows. They do not replace honest field display, circuit breakers, or long-term VIN result caches.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const a = mintDecodeAttempt("1HGCM82633A004352");
const b = mintDecodeAttempt("1HGCM82633A004352");
assert.notEqual(a.idempotencyKey, b.idempotencyKey);

assert.equal(gateUpstream(a.idempotencyKey).kind, "proceed");
assert.equal(gateUpstream(a.idempotencyKey).kind, "inflight");
completeAttempt(a.idempotencyKey, { make: "HONDA" });
const replay = gateUpstream(a.idempotencyKey);
assert.equal(replay.kind, "replay");

const headers = headerForAttempt(a);
assert.equal(
  (headers as Record&lt;string, string&gt;)["Idempotency-Key"],
  a.idempotencyKey,
);
</code></pre>
<p>Review rule: retry helpers must accept an existing attempt object; they must not mint inside the loop.</p>
<h2>Takeaway</h2>
<p>Client idempotency keys stop retry storms and double-clicks from burning NHTSA quota twice for one decode intent. Mint once per tap, send the same header on every retry, gate upstream before <code>DecodeVinValues</code>, and expire keys quickly so deliberate refreshes remain real. Handler idempotency and VIN caches remain useful -- they solve different windows. Quota honesty starts at the client key.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/client-idempotency-keys-for-vin-decode-retries-without-double-charging-upstream-quotas-2ap">https://dev.to/vin_lookup_8dbd4710f77e9e/client-idempotency-keys-for-vin-decode-retries-without-double-charging-upstream-quotas-2ap</a></p>
]]></content:encoded></item><item><title><![CDATA[Showing ESC from vPIC Without Inventing Stability-Control Safety Grades]]></title><description><![CDATA[NHTSA vPIC often returns electronic stability control catalog fields on DecodeVinValues-style payloads -- commonly under keys such as ESC when the pattern includes that token. Those strings are useful]]></description><link>https://freevinlookup.hashnode.dev/showing-esc-from-vpic-without-inventing-stability-control-safety-grades</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/showing-esc-from-vpic-without-inventing-stability-control-safety-grades</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[cars]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:23:17 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC often returns electronic stability control catalog fields on DecodeVinValues-style payloads -- commonly under keys such as <code>ESC</code> when the pattern includes that token. Those strings are useful on a free VIN decode card. The trap is turning a single sourced field into a stability-control safety grade, "ESC verified working," or skid-prevention claim that the catalog never asserted.</p>
<p>This post is about honest display: show the ESC catalog value when vPIC provides it, refuse stability-safety inventing, and allow a clean "not provided" state when the field is empty. Keep ESC catalog tokens separate from yaw-rate sensor health, brake-pressure tests, and road-course handling scores. A sourced token is still only a token: it does not prove the system is present, calibrated, or undamaged on the listed vehicle. (ABS honesty is a separate topic; here the focus is ESC.)</p>
<h2>What ESC is (and is not) in vPIC</h2>
<p><code>ESC</code> (or the equivalent electronic stability control field your decode maps) is a catalog attribute associated with the VIN pattern. It answers a narrow question: which ESC-related token did the decode associate with this pattern?</p>
<p>It does <strong>not</strong> answer:</p>
<ul>
<li>Whether the vehicle currently meets a stability or rollover performance standard</li>
<li>Whether ESC modules, yaw sensors, or hydraulic actuators work on this specific vehicle today</li>
<li>Traction control, ABS, or brake-assist packages you did not source as separate keys</li>
<li>Whether a dashboard ESC light is on, off, or silenced after a repair</li>
<li>A composite "stability safety score" built from missing neighboring fields</li>
</ul>
<p>Empty ESC does not authorize a default "Standard" badge, and a positive token does not mean "skid control verified." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by model year.</p>
<h2>Normalize empties, do not invent grades</h2>
<p>Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced string when present; do <strong>not</strong> rewrite it into "ESC Grade A" or a stability-inspection score.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type EscView = {
  value: string | null;
  source: "vpic" | "missing";
};

export function viewEsc(fields: {
  ESC?: string | null;
}): EscView {
  const raw = (fields.ESC ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { value: null, source: "missing" };
  }
  return { value: raw, source: "vpic" };
}

export function escLines(view: EscView): string[] {
  if (view.source === "missing") {
    return ["ESC: not provided by vPIC for this VIN"];
  }
  return [
    `ESC (vPIC): ${view.value}`,
    "Catalog token only -- not a stability-control safety grade",
  ];
}
</code></pre>
<p>The footnote matters. Buyers over-read a single ESC field as verified skid prevention. Show the sourced token; if blank, say "not provided" -- no greyed "Standard" and no invented stability grade.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Mapping any non-empty ESC into "electronic stability control verified"</li>
<li>Inventing IIHS / Euro NCAP / star-equivalent stability-safety language from the catalog field</li>
<li>Bundling ESC with ABS or traction-control keys into "Full Stability Safety Package"</li>
<li>Defaulting blank ESC to "Standard" so mid-trim cars look complete</li>
<li>Renaming catalog tokens into OEM chassis-suite marketing names you did not source</li>
</ol>
<p>Refuse those. If you show neighboring catalog fields from separate sourced keys, show each on its own row. Never invent skid-prevention or package language inside the ESC mapper.</p>
<pre><code class="language-ts">export function assertNoEscStabilityInvent(moduleSource: string): void {
  const banned = [
    /esc verified/i,
    /stability.?control safety grade/i,
    /skid prevention verified/i,
    /iihs/i,
    /full stability safety/i,
    /euro ncap/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `ESC module must not invent stability grades: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  ESC?: string | null;
  ABS?: string | null;
  TractionControl?: string | null;
}): CardRow[] {
  const view = viewEsc(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({ label: "ESC", value: "not provided" });
  } else {
    out.push({ label: "ESC", value: String(view.value) });
  }

  const abs = (fields.ABS ?? "").trim();
  if (abs &amp;&amp; !EMPTY.has(abs.toLowerCase())) {
    out.push({ label: "ABS (vPIC)", value: abs });
  }
  const tc = (fields.TractionControl ?? "").trim();
  if (tc &amp;&amp; !EMPTY.has(tc.toLowerCase())) {
    out.push({ label: "Traction control (vPIC)", value: tc });
  }
  return out;
}
</code></pre>
<p>Keep neighboring catalog fields on their own labeled rows. Never concatenate them into "Full Stability Safety Package," and never treat a separate ABS catalog key as proof that ESC implies a safety grade.</p>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"ESC (vPIC): Standard"</li>
<li>"ESC: not provided by vPIC for this VIN"</li>
<li>A short footnote: catalog token, not a stability-control safety grade</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"IIHS stability Superior -- verified"</li>
<li>"ESC Grade A included"</li>
<li>Grey placeholders that look like real ESC data when the field was empty</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

assert.equal(viewEsc({}).source, "missing");
assert.deepEqual(escLines(viewEsc({})), [
  "ESC: not provided by vPIC for this VIN",
]);

const std = viewEsc({ ESC: "Standard" });
assert.equal(std.value, "Standard");
assert.ok(
  escLines(std).some((l) =&gt; /not a stability-control safety grade/i.test(l)),
);
assert.ok(
  !escLines(std).some((l) =&gt;
    /iihs|esc verified|stability safety package/i.test(l),
  ),
);

const rows = cardRows({
  ESC: "",
  ABS: "Standard",
  TractionControl: "Standard",
});
assert.ok(
  rows.some((r) =&gt; r.label === "ESC" &amp;&amp; r.value === "not provided"),
);
assert.ok(rows.some((r) =&gt; r.label.includes("ABS")));
assert.ok(rows.some((r) =&gt; r.label.includes("Traction")));
assert.ok(!rows.some((r) =&gt; /iihs|esc verified/i.test(r.value)));
</code></pre>
<p>Review rule: ESC modules must not contain stability-upgrade or safety-grade phrases except in forbidding tests. Never upgrade a blank into "Standard."</p>
<h2>Takeaway</h2>
<p><code>ESC</code> is a catalog token. Display it with clear sourcing, allow "not provided," and never use it as a key into invented stability-control safety grades, skid-prevention claims, or "full stability safety" bundles. Your free VIN UI stays trustworthy when ESC data is either a real vPIC value with a modest footnote -- or absent -- and chassis-performance marketing lives somewhere else, clearly labeled, or not at all.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/showing-esc-from-vpic-without-inventing-stability-control-safety-grades-13c1">https://dev.to/vin_lookup_8dbd4710f77e9e/showing-esc-from-vpic-without-inventing-stability-control-safety-grades-13c1</a></p>
]]></content:encoded></item><item><title><![CDATA[Displaying AdaptiveCruiseControl from vPIC Without Inventing Autonomy Claims]]></title><description><![CDATA[NHTSA vPIC often returns adaptive-cruise catalog fields on DecodeVinValues-style payloads -- including AdaptiveCruiseControl when the pattern includes that token. Those strings are useful on a free VI]]></description><link>https://freevinlookup.hashnode.dev/displaying-adaptivecruisecontrol-from-vpic-without-inventing-autonomy-claims</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/displaying-adaptivecruisecontrol-from-vpic-without-inventing-autonomy-claims</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[cars]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:17:46 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC often returns adaptive-cruise catalog fields on DecodeVinValues-style payloads -- including <code>AdaptiveCruiseControl</code> when the pattern includes that token. Those strings are useful on a free VIN decode card. The trap is turning a single sourced field into a Level-2 autonomy claim, "self-driving capable," or hands-free highway package that the catalog never asserted.</p>
<p>This post is about honest display: show <code>AdaptiveCruiseControl</code> when vPIC provides it, refuse autonomy inventing, and allow a clean "not provided" state when the field is empty. Keep adaptive-cruise catalog values separate from SAE automation levels, OEM suite marketing, and highway-pilot branding. A sourced token is still only a token: it does not prove the feature is present, calibrated, or undamaged on the listed vehicle. Do not upgrade a cruise catalog string into a driverless narrative. (Forward-collision honesty is a separate topic; here the focus is AdaptiveCruiseControl.)</p>
<h2>What AdaptiveCruiseControl is (and is not)</h2>
<p><code>AdaptiveCruiseControl</code> is a catalog attribute associated with the VIN decode. It answers a narrow question: which adaptive-cruise token did the decode associate with this pattern?</p>
<p>It does <strong>not</strong> answer:</p>
<ul>
<li>Whether the vehicle is SAE Level 2, Level 3, or "autonomous"</li>
<li>Whether lane-centering, hands-free, or traffic-jam assist is included</li>
<li>Radar vs camera hardware, following-distance modes, or stop-and-go behavior you did not source</li>
<li>Whether the feature works today, was optioned, or was deleted after manufacture</li>
<li>A composite "autonomy score" built from missing neighboring fields</li>
</ul>
<p>Empty <code>AdaptiveCruiseControl</code> does not authorize a default "Standard" badge, and a positive token does not mean "self-driving verified." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by model year.</p>
<h2>Normalize empties, do not invent autonomy</h2>
<p>Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced string when present; do <strong>not</strong> rewrite it into "Level 2 Standard" or an autonomy marketing grade.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type AccView = {
  value: string | null;
  source: "vpic" | "missing";
};

export function viewAdaptiveCruiseControl(fields: {
  AdaptiveCruiseControl?: string | null;
}): AccView {
  const raw = (fields.AdaptiveCruiseControl ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { value: null, source: "missing" };
  }
  return { value: raw, source: "vpic" };
}

export function adaptiveCruiseLines(view: AccView): string[] {
  if (view.source === "missing") {
    return [
      "Adaptive cruise control: not provided by vPIC for this VIN",
    ];
  }
  return [
    `Adaptive cruise control (vPIC): ${view.value}`,
    "Catalog token only -- not an autonomy or SAE level claim",
  ];
}
</code></pre>
<p>The footnote matters. Buyers over-read a single adaptive-cruise field as verified hands-free driving. Show the sourced token; if blank, say "not provided" -- no greyed "Standard" and no invented autonomy claim.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Mapping any non-empty AdaptiveCruiseControl into "Level 2 included"</li>
<li>Inventing SAE / NHTSA automation-level language from the catalog field</li>
<li>Bundling AdaptiveCruiseControl with lane-keep keys into "Full Autonomy Package"</li>
<li>Defaulting blank AdaptiveCruiseControl to "Standard" so mid-trim cars look complete</li>
<li>Renaming catalog tokens into OEM highway-pilot marketing names you did not source</li>
</ol>
<p>Refuse those. If you show other ADAS catalog fields from separate sourced keys, show each on its own row. Never invent SAE levels or package language inside the AdaptiveCruiseControl mapper.</p>
<pre><code class="language-ts">export function assertNoAccAutonomyInvent(moduleSource: string): void {
  const banned = [
    /level\s*[123]/i,
    /self-?driving/i,
    /autonom(y|ous)/i,
    /hands-?free/i,
    /full autonomy package/i,
    /sae\s*level/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `AdaptiveCruiseControl module must not invent autonomy: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  AdaptiveCruiseControl?: string | null;
  ForwardCollisionWarning?: string | null;
  LaneKeepingSystem?: string | null;
}): CardRow[] {
  const view = viewAdaptiveCruiseControl(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({
      label: "Adaptive cruise control",
      value: "not provided",
    });
  } else {
    out.push({
      label: "Adaptive cruise control",
      value: String(view.value),
    });
  }

  const fcw = (fields.ForwardCollisionWarning ?? "").trim();
  if (fcw &amp;&amp; !EMPTY.has(fcw.toLowerCase())) {
    out.push({ label: "Forward collision (vPIC)", value: fcw });
  }
  const lks = (fields.LaneKeepingSystem ?? "").trim();
  if (lks &amp;&amp; !EMPTY.has(lks.toLowerCase())) {
    out.push({ label: "Lane keeping (vPIC)", value: lks });
  }
  return out;
}
</code></pre>
<p>Keep neighboring catalog fields on their own labeled rows. Never concatenate them into "Full Autonomy Package," and never treat a separate lane-keeping catalog key as proof that AdaptiveCruiseControl implies SAE Level 2.</p>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"Adaptive cruise control (vPIC): Standard"</li>
<li>"Adaptive cruise control: not provided by vPIC for this VIN"</li>
<li>A short footnote: catalog token, not an autonomy or SAE level claim</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"SAE Level 2 -- verified"</li>
<li>"Self-driving / hands-free included"</li>
<li>Grey placeholders that look like real adaptive-cruise data when the field was empty</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

assert.equal(viewAdaptiveCruiseControl({}).source, "missing");
assert.deepEqual(
  adaptiveCruiseLines(viewAdaptiveCruiseControl({})),
  ["Adaptive cruise control: not provided by vPIC for this VIN"],
);

const std = viewAdaptiveCruiseControl({
  AdaptiveCruiseControl: "Standard",
});
assert.equal(std.value, "Standard");
assert.ok(
  adaptiveCruiseLines(std).some((l) =&gt;
    /not an autonomy or SAE level/i.test(l),
  ),
);
assert.ok(
  !adaptiveCruiseLines(std).some((l) =&gt;
    /level\s*2|self-driving|autonomy package/i.test(l),
  ),
);

const rows = cardRows({
  AdaptiveCruiseControl: "",
  ForwardCollisionWarning: "Standard",
  LaneKeepingSystem: "Standard",
});
assert.ok(
  rows.some(
    (r) =&gt;
      r.label.includes("Adaptive cruise") &amp;&amp; r.value === "not provided",
  ),
);
assert.ok(rows.some((r) =&gt; r.label.includes("Forward collision")));
assert.ok(rows.some((r) =&gt; r.label.includes("Lane keeping")));
assert.ok(!rows.some((r) =&gt; /level\s*2|self-driving/i.test(r.value)));
</code></pre>
<p>Review rule: adaptive-cruise modules must not contain SAE-upgrade or autonomy-package phrases except in forbidding tests. Never upgrade a blank into "Standard."</p>
<h2>Takeaway</h2>
<p><code>AdaptiveCruiseControl</code> is a catalog token. Display it with clear sourcing, allow "not provided," and never use it as a key into invented SAE levels, self-driving claims, or "full autonomy" bundles. Your free VIN UI stays trustworthy when adaptive-cruise data is either a real vPIC value with a modest footnote -- or absent -- and autonomy marketing lives somewhere else, clearly labeled, or not at all.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/displaying-adaptivecruisecontrol-from-vpic-without-inventing-autonomy-claims-57eh">https://dev.to/vin_lookup_8dbd4710f77e9e/displaying-adaptivecruisecontrol-from-vpic-without-inventing-autonomy-claims-57eh</a></p>
]]></content:encoded></item><item><title><![CDATA[Circuit-Breaking NHTSA Calls So One Slow Upstream Does Not Stall Every VIN Form]]></title><description><![CDATA[Error-rate breakers trip when NHTSA is failing hard. A quieter failure mode is worse for forms: the upstream is merely slow. One hung DecodeVinValues call holds a shared client, saturates your concurr]]></description><link>https://freevinlookup.hashnode.dev/circuit-breaking-nhtsa-calls-so-one-slow-upstream-does-not-stall-every-vin-form</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/circuit-breaking-nhtsa-calls-so-one-slow-upstream-does-not-stall-every-vin-form</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:12:22 GMT</pubDate><content:encoded><![CDATA[<p>Error-rate breakers trip when NHTSA is failing hard. A quieter failure mode is worse for forms: the upstream is merely <strong>slow</strong>. One hung <code>DecodeVinValues</code> call holds a shared client, saturates your concurrency budget, and every other VIN submit waits behind a spinner that never resolves. Retries with longer timeouts make the pile deeper.</p>
<p>This post is about latency-aware circuit breaking for free VIN decode: treat sustained slowness as a trip condition, fail fast for concurrent forms while open, and keep one slow call from stalling the whole UI surface. Classic consecutive-failure breakers are covered elsewhere; here the focus is <strong>slow upstream stalls every form</strong>.</p>
<h2>Slow is not the same as down</h2>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Typical user experience</th>
<th>Breaker role</th>
</tr>
</thead>
<tbody><tr>
<td>Hard 5xx / network error</td>
<td>Immediate error card</td>
<td>Count as failure</td>
</tr>
<tr>
<td>Sparse 200 with Make/Model</td>
<td>Partial but usable card</td>
<td>Success (not a trip)</td>
</tr>
<tr>
<td>Hang past budget</td>
<td>Spinner forever; other forms blocked</td>
<td>Count as <strong>slow failure</strong></td>
</tr>
<tr>
<td>Burst of 8s+ latencies</td>
<td>Queue of submits backs up</td>
<td>Open on rate of slow calls</td>
</tr>
</tbody></table>
<p>A form that shares one decode client (module singleton, server action pool, or browser fetch queue) is especially fragile. Without a breaker, the first hung request occupies the only in-flight slot; the second submit waits; the third looks "broken" even though validation is fine.</p>
<h2>What should count as slow failure</h2>
<p>Define an explicit per-call budget (for example 6â€“8s wall time). When the call exceeds the budget -- via <code>AbortSignal.timeout</code>, a racing <code>Promise</code>, or your HTTP client's deadline -- record a <strong>slow failure</strong> for the breaker, abort the in-flight work, and return a typed <code>UPSTREAM_SLOW</code> result.</p>
<p>Count toward the trip:</p>
<ul>
<li>Timeouts and deadline aborts</li>
<li>Network errors that leave the request unresolved</li>
<li>HTTP 502 / 503 / 504 after a long wait (still a failure; also slow)</li>
</ul>
<p>Do <strong>not</strong> count:</p>
<ul>
<li>Local validation rejects (never called upstream)</li>
<li>Fast 200s with empty ABS / Make gaps (honest sparse success)</li>
<li>User-aborted navigations when you already cancelled cleanly</li>
</ul>
<p>Mixing validation noise into slow-failure counts opens the circuit on bad paste days and blocks good VINs for no reason.</p>
<h2>TypeScript sketch: latency trips + form gate</h2>
<pre><code class="language-ts">type BreakerState = "closed" | "open" | "half_open";

export type LatencyBreakerConfig = {
  slowThresholdMs: number; // e.g. 7000
  failureThreshold: number; // e.g. 3 consecutive slow/hard failures
  cooldownMs: number; // e.g. 20_000
  halfOpenMaxProbes: number; // e.g. 1
};

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

  constructor(private readonly cfg: LatencyBreakerConfig) {}

  canRequest(): boolean {
    if (this.state === "closed") return true;
    if (this.state === "open") {
      if (Date.now() - this.openedAt &gt;= this.cfg.cooldownMs) {
        this.state = "half_open";
        this.halfOpenProbes = 0;
        return true;
      }
      return false;
    }
    return this.halfOpenProbes &lt; 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 &gt;= this.cfg.failureThreshold
    ) {
      this.state = "open";
      this.openedAt = Date.now();
      this.halfOpenProbes = 0;
    }
  }
}

export type DecodeResult&lt;T&gt; =
  | { ok: true; data: T }
  | { ok: false; reason: "circuit_open" | "upstream_slow" | "upstream" };

export async function decodeWithLatencyBreaker&lt;T&gt;(
  breaker: LatencyCircuitBreaker,
  call: (signal: AbortSignal) =&gt; Promise&lt;T&gt;,
  cfg: { slowThresholdMs: number },
): Promise&lt;DecodeResult&lt;T&gt;&gt; {
  if (!breaker.canRequest()) {
    return { ok: false, reason: "circuit_open" };
  }
  breaker.beforeRequest();
  const controller = new AbortController();
  const timer = setTimeout(
    () =&gt; controller.abort(),
    cfg.slowThresholdMs,
  );
  const started = Date.now();
  try {
    const data = await call(controller.signal);
    clearTimeout(timer);
    breaker.recordSuccess();
    return { ok: true, data };
  } catch (err) {
    clearTimeout(timer);
    breaker.recordFailure();
    const timedOut =
      controller.signal.aborted &amp;&amp;
      Date.now() - started &gt;= cfg.slowThresholdMs - 50;
    return {
      ok: false,
      reason: timedOut ? "upstream_slow" : "upstream",
    };
  }
}
</code></pre>
<p>Gate every form submit on <code>canRequest()</code> <strong>before</strong> disabling inputs or starting a spinner. When the circuit is open, return immediately with <code>circuit_open</code> so sibling forms stay interactive.</p>
<h2>UX that does not stall the form</h2>
<p>Prefer:</p>
<ul>
<li>Instant "decode temporarily unavailable -- upstream is slow" when open</li>
<li>Disable only the submit that would call NHTSA; keep paste/clear/help controls alive</li>
<li>Optional stale last-good decode for that VIN, labeled "cached earlier; live refresh paused"</li>
</ul>
<p>Avoid:</p>
<ul>
<li>A global page-level spinner held by one hung fetch</li>
<li>Mapping <code>circuit_open</code> or <code>upstream_slow</code> to "invalid VIN"</li>
<li>Silent retries that re-queue the same hung call behind the open circuit</li>
</ul>
<p>Keep reason codes separate (<code>CIRCUIT_OPEN</code> vs <code>UPSTREAM_SLOW</code> vs <code>VIN_INVALID</code>) so support and status copy stay accurate.</p>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const breaker = new LatencyCircuitBreaker({
  slowThresholdMs: 50,
  failureThreshold: 2,
  cooldownMs: 60_000,
  halfOpenMaxProbes: 1,
});

async function hang(_signal: AbortSignal): Promise&lt;never&gt; {
  await new Promise(() =&gt; {});
  throw new Error("unreachable");
}

const a = await decodeWithLatencyBreaker(breaker, hang, {
  slowThresholdMs: 50,
});
assert.equal(a.ok, false);
if (!a.ok) assert.equal(a.reason, "upstream_slow");

const b = await decodeWithLatencyBreaker(breaker, hang, {
  slowThresholdMs: 50,
});
assert.equal(b.ok, false);

assert.equal(breaker.canRequest(), false);
const blocked = await decodeWithLatencyBreaker(breaker, hang, {
  slowThresholdMs: 50,
});
assert.deepEqual(blocked, { ok: false, reason: "circuit_open" });
</code></pre>
<p>Review rule: form submit handlers must short-circuit on open before awaiting upstream. Never let one slow call own the only concurrency slot without a budget.</p>
<h2>Takeaway</h2>
<p>One slow NHTSA call should not freeze every VIN form. Budget each decode, count timeouts as failures, open the circuit before the queue piles up, and fail fast with an honest "temporarily unavailable" state. Layer latency breakers with validation and single-flight, and free VIN decode stays responsive when the public API merely crawls instead of crashing.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/circuit-breaking-nhtsa-calls-so-one-slow-upstream-does-not-stall-every-vin-form-2clc">https://dev.to/vin_lookup_8dbd4710f77e9e/circuit-breaking-nhtsa-calls-so-one-slow-upstream-does-not-stall-every-vin-form-2clc</a></p>
]]></content:encoded></item><item><title><![CDATA[Showing ABS from vPIC Without Inventing Brake-System Safety Grades]]></title><description><![CDATA[NHTSA vPIC often returns anti-lock brake catalog fields on DecodeVinValues-style payloads -- commonly under keys such as ABS when the pattern includes that token. Those strings are useful on a free VI]]></description><link>https://freevinlookup.hashnode.dev/showing-abs-from-vpic-without-inventing-brake-system-safety-grades</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/showing-abs-from-vpic-without-inventing-brake-system-safety-grades</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[cars]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:07:05 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC often returns anti-lock brake catalog fields on DecodeVinValues-style payloads -- commonly under keys such as <code>ABS</code> when the pattern includes that token. Those strings are useful on a free VIN decode card. The trap is turning a single sourced field into a brake-system safety grade, "ABS verified working," or stopping-distance claim that the catalog never asserted.</p>
<p>This post is about honest display: show the ABS catalog value when vPIC provides it, refuse brake-safety inventing, and allow a clean "not provided" state when the field is empty. Keep ABS catalog tokens separate from brake-pad wear, hydraulic pressure, and road-test stopping distances. A sourced token is still only a token: it does not prove the system is present, calibrated, or undamaged on the listed vehicle. (TPMS and seat-belt honesty are separate topics; here the focus is ABS.)</p>
<h2>What ABS is (and is not) in vPIC</h2>
<p><code>ABS</code> (or the equivalent anti-lock brake field your decode maps) is a catalog attribute associated with the VIN pattern. It answers a narrow question: which ABS-related token did the decode associate with this pattern?</p>
<p>It does <strong>not</strong> answer:</p>
<ul>
<li>Whether brakes currently meet a stopping-distance or fade standard</li>
<li>Whether ABS modules, sensors, or pumps work on this specific vehicle today</li>
<li>Electronic stability control, traction control, or brake-assist packages you did not source</li>
<li>Whether a dashboard ABS light is on, off, or silenced after a repair</li>
<li>A composite "brake safety score" built from missing neighboring fields</li>
</ul>
<p>Empty ABS does not authorize a default "Standard" badge, and a positive token does not mean "brakes verified safe." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by model year.</p>
<h2>Normalize empties, do not invent grades</h2>
<p>Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced string when present; do <strong>not</strong> rewrite it into "ABS Grade A" or a brake-inspection score.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type AbsView = {
  value: string | null;
  source: "vpic" | "missing";
};

export function viewAbs(fields: {
  ABS?: string | null;
}): AbsView {
  const raw = (fields.ABS ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { value: null, source: "missing" };
  }
  return { value: raw, source: "vpic" };
}

export function absLines(view: AbsView): string[] {
  if (view.source === "missing") {
    return ["ABS: not provided by vPIC for this VIN"];
  }
  return [
    `ABS (vPIC): ${view.value}`,
    "Catalog token only -- not a brake-system safety grade",
  ];
}
</code></pre>
<p>The footnote matters. Buyers over-read a single ABS field as verified stopping performance. Show the sourced token; if blank, say "not provided" -- no greyed "Standard" and no invented brake grade.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Mapping any non-empty ABS into "anti-lock brakes verified"</li>
<li>Inventing IIHS / Euro NCAP / star-equivalent brake-safety language from the catalog field</li>
<li>Bundling ABS with ESC or traction-control keys into "Full Brake Safety Package"</li>
<li>Defaulting blank ABS to "Standard" so mid-trim cars look complete</li>
<li>Renaming catalog tokens into OEM brake-suite marketing names you did not source</li>
</ol>
<p>Refuse those. If you show neighboring catalog fields from separate sourced keys, show each on its own row. Never invent stopping-distance or package language inside the ABS mapper.</p>
<pre><code class="language-ts">export function assertNoAbsBrakeInvent(moduleSource: string): void {
  const banned = [
    /abs verified/i,
    /brake.?system safety grade/i,
    /stopping distance/i,
    /iihs/i,
    /full brake safety/i,
    /euro ncap/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `ABS module must not invent brake grades: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  ABS?: string | null;
  ESC?: string | null;
  TractionControl?: string | null;
}): CardRow[] {
  const view = viewAbs(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({ label: "ABS", value: "not provided" });
  } else {
    out.push({ label: "ABS", value: String(view.value) });
  }

  const esc = (fields.ESC ?? "").trim();
  if (esc &amp;&amp; !EMPTY.has(esc.toLowerCase())) {
    out.push({ label: "ESC (vPIC)", value: esc });
  }
  const tc = (fields.TractionControl ?? "").trim();
  if (tc &amp;&amp; !EMPTY.has(tc.toLowerCase())) {
    out.push({ label: "Traction control (vPIC)", value: tc });
  }
  return out;
}
</code></pre>
<p>Keep neighboring catalog fields on their own labeled rows. Never concatenate them into "Full Brake Safety Package," and never treat a separate ESC catalog key as proof that ABS implies a safety grade.</p>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"ABS (vPIC): Standard"</li>
<li>"ABS: not provided by vPIC for this VIN"</li>
<li>A short footnote: catalog token, not a brake-system safety grade</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"IIHS brake safety Superior -- verified"</li>
<li>"ABS Grade A included"</li>
<li>Grey placeholders that look like real ABS data when the field was empty</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

assert.equal(viewAbs({}).source, "missing");
assert.deepEqual(absLines(viewAbs({})), [
  "ABS: not provided by vPIC for this VIN",
]);

const std = viewAbs({ ABS: "Standard" });
assert.equal(std.value, "Standard");
assert.ok(
  absLines(std).some((l) =&gt; /not a brake-system safety grade/i.test(l)),
);
assert.ok(
  !absLines(std).some((l) =&gt;
    /iihs|abs verified|brake safety package/i.test(l),
  ),
);

const rows = cardRows({
  ABS: "",
  ESC: "Standard",
  TractionControl: "Standard",
});
assert.ok(
  rows.some((r) =&gt; r.label === "ABS" &amp;&amp; r.value === "not provided"),
);
assert.ok(rows.some((r) =&gt; r.label.includes("ESC")));
assert.ok(rows.some((r) =&gt; r.label.includes("Traction")));
assert.ok(!rows.some((r) =&gt; /iihs|abs verified/i.test(r.value)));
</code></pre>
<p>Review rule: ABS modules must not contain brake-upgrade or safety-grade phrases except in forbidding tests. Never upgrade a blank into "Standard."</p>
<h2>Takeaway</h2>
<p><code>ABS</code> is a catalog token. Display it with clear sourcing, allow "not provided," and never use it as a key into invented brake-system safety grades, stopping-distance claims, or "full brake safety" bundles. Your free VIN UI stays trustworthy when ABS data is either a real vPIC value with a modest footnote -- or absent -- and brake-performance marketing lives somewhere else, clearly labeled, or not at all.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/showing-abs-from-vpic-without-inventing-brake-system-safety-grades-50jc">https://dev.to/vin_lookup_8dbd4710f77e9e/showing-abs-from-vpic-without-inventing-brake-system-safety-grades-50jc</a></p>
]]></content:encoded></item><item><title><![CDATA[Displaying TPMS from vPIC Without Inventing Tire-Safety Claims]]></title><description><![CDATA[NHTSA vPIC often returns tire-pressure monitoring catalog fields on DecodeVinValues-style payloads -- commonly under keys such as TPMS when the pattern includes that token. Those strings are useful on]]></description><link>https://freevinlookup.hashnode.dev/displaying-tpms-from-vpic-without-inventing-tire-safety-claims</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/displaying-tpms-from-vpic-without-inventing-tire-safety-claims</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[cars]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 14:01:33 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC often returns tire-pressure monitoring catalog fields on DecodeVinValues-style payloads -- commonly under keys such as <code>TPMS</code> when the pattern includes that token. Those strings are useful on a free VIN decode card. The trap is turning a single sourced field into a tire-health grade, "TPMS working," or roadside-safety claim that the catalog never asserted.</p>
<p>This post is about honest display: show the TPMS catalog value when vPIC provides it, refuse tire-safety inventing, and allow a clean "not provided" state when the field is empty. Keep TPMS catalog tokens separate from live sensor readings, tire tread depth, and inspection results. A sourced token is still only a token: it does not prove the system is present, calibrated, or undamaged on the listed vehicle.</p>
<h2>What TPMS is (and is not) in vPIC</h2>
<p><code>TPMS</code> (or the equivalent tire-pressure monitoring field your decode maps) is a catalog attribute associated with the VIN pattern. It answers a narrow question: which TPMS-related token did the decode associate with this pattern?</p>
<p>It does <strong>not</strong> answer:</p>
<ul>
<li>Whether tires currently hold safe pressure or need inflation</li>
<li>Whether sensors are installed, paired, or reporting correctly today</li>
<li>Direct vs indirect TPMS hardware details you did not source</li>
<li>Whether a dashboard light is on, off, or silenced after a wheel swap</li>
<li>A composite "tire safety score" built from missing neighboring fields</li>
</ul>
<p>Empty TPMS does not authorize a default "Standard" badge, and a positive token does not mean "tires verified safe." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by model year.</p>
<h2>Normalize empties, do not invent health</h2>
<p>Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced string when present; do <strong>not</strong> rewrite it into "TPMS Healthy" or a tire-inspection grade.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type TpmsView = {
  value: string | null;
  source: "vpic" | "missing";
};

export function viewTpms(fields: {
  TPMS?: string | null;
}): TpmsView {
  const raw = (fields.TPMS ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { value: null, source: "missing" };
  }
  return { value: raw, source: "vpic" };
}

export function tpmsLines(view: TpmsView): string[] {
  if (view.source === "missing") {
    return ["TPMS: not provided by vPIC for this VIN"];
  }
  return [
    `TPMS (vPIC): ${view.value}`,
    "Catalog token only -- not a tire-health or sensor status claim",
  ];
}
</code></pre>
<p>The footnote matters. Buyers over-read a TPMS catalog field as proof that tires are safe to drive. Show the sourced token; if blank, say "not provided" -- no greyed "Direct" and no invented "pressure OK" badge.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Mapping any non-empty TPMS into "Tire pressure monitoring working"</li>
<li>Inventing psi readings, tread depth, or inspection grades from the catalog field</li>
<li>Bundling TPMS with unrelated safety keys into "Full Tire Safety Package"</li>
<li>Defaulting blank TPMS to "Standard" so mid-trim cars look complete</li>
<li>Renaming catalog tokens into OEM sensor marketing names you did not source</li>
</ol>
<p>Refuse those. If you show other catalog fields from separate sourced keys, show each on its own row. Never invent live tire health inside the TPMS mapper.</p>
<pre><code class="language-ts">export function assertNoTpmsHealthInvent(moduleSource: string): void {
  const banned = [
    /tire.?health/i,
    /pressure ok/i,
    /psi verified/i,
    /tread depth/i,
    /sensors working/i,
    /full tire safety/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `TPMS module must not invent tire-safety claims: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  TPMS?: string | null;
  TirePressureMonitoringType?: string | null;
  WheelBaseType?: string | null;
}): CardRow[] {
  const view = viewTpms(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({ label: "TPMS", value: "not provided" });
  } else {
    out.push({ label: "TPMS", value: String(view.value) });
  }

  const kind = (fields.TirePressureMonitoringType ?? "").trim();
  if (kind &amp;&amp; !EMPTY.has(kind.toLowerCase())) {
    out.push({
      label: "TPMS type (vPIC)",
      value: kind,
    });
  }
  const wheel = (fields.WheelBaseType ?? "").trim();
  if (wheel &amp;&amp; !EMPTY.has(wheel.toLowerCase())) {
    out.push({ label: "Wheel base type (vPIC)", value: wheel });
  }
  return out;
}
</code></pre>
<p>Keep neighboring catalog fields on their own labeled rows. Never concatenate them into "Full Tire Safety Package," and never treat a separate type key as live sensor telemetry.</p>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"TPMS (vPIC): Direct"</li>
<li>"TPMS: not provided by vPIC for this VIN"</li>
<li>A short footnote: catalog token, not a tire-health or sensor status claim</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"Tire pressure OK -- verified"</li>
<li>"Sensors working / full tire safety included"</li>
<li>Grey placeholders that look like real TPMS data when the field was empty</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

assert.equal(viewTpms({}).source, "missing");
assert.deepEqual(tpmsLines(viewTpms({})), [
  "TPMS: not provided by vPIC for this VIN",
]);

const direct = viewTpms({ TPMS: "Direct" });
assert.equal(direct.value, "Direct");
assert.ok(
  tpmsLines(direct).some((l) =&gt;
    /not a tire-health or sensor status/i.test(l),
  ),
);
assert.ok(
  !tpmsLines(direct).some((l) =&gt;
    /pressure ok|tread|sensors working/i.test(l),
  ),
);

const rows = cardRows({
  TPMS: "",
  TirePressureMonitoringType: "Direct",
  WheelBaseType: "Long",
});
assert.ok(
  rows.some((r) =&gt; r.label === "TPMS" &amp;&amp; r.value === "not provided"),
);
assert.ok(rows.some((r) =&gt; r.label.includes("TPMS type")));
assert.ok(rows.some((r) =&gt; r.label.includes("Wheel base")));
assert.ok(!rows.some((r) =&gt; /pressure ok|tire health/i.test(r.value)));
</code></pre>
<p>Review rule: TPMS modules must not contain tire-health or live-sensor phrases except in forbidding tests. Never upgrade a blank into "Standard" or "Direct."</p>
<h2>Takeaway</h2>
<p>TPMS from vPIC is a catalog token. Display it with clear sourcing, allow "not provided," and never use it as a key into invented psi readings, sensor-working claims, or "full tire safety" bundles. Your free VIN UI stays trustworthy when TPMS data is either a real vPIC value with a modest footnote -- or absent -- and tire inspection marketing lives somewhere else, clearly labeled, or not at all.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/displaying-tpms-from-vpic-without-inventing-tire-safety-claims-4me6">https://dev.to/vin_lookup_8dbd4710f77e9e/displaying-tpms-from-vpic-without-inventing-tire-safety-claims-4me6</a></p>
]]></content:encoded></item><item><title><![CDATA[Handling Truncated or Partial vPIC JSON Without Displaying Half-Invented Vehicles]]></title><description><![CDATA[NHTSA vPIC DecodeVinValues responses are JSON. Most of the time the body is complete: you JSON.parse, read Results[0], and map catalog fields. The trap is treating a truncated body, a cut-off stream, ]]></description><link>https://freevinlookup.hashnode.dev/handling-truncated-or-partial-vpic-json-without-displaying-half-invented-vehicles</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/handling-truncated-or-partial-vpic-json-without-displaying-half-invented-vehicles</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cars]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 13:56:14 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC DecodeVinValues responses are JSON. Most of the time the body is complete: you <code>JSON.parse</code>, read <code>Results[0]</code>, and map catalog fields. The trap is treating a truncated body, a cut-off stream, or a half-parsed object as a successful vehicle card. A free VIN UI that paints Make from a broken payload while inventing Model trains buyers to trust garbage.</p>
<p>This post is about parse honesty: refuse to render incomplete vehicle identity as success, distinguish transport truncation from sparse-but-valid catalog rows, and guard <code>JSON.parse</code> plus result shape. Sparse fields after a valid parse are a different topic; here the failure mode is broken or partial JSON itself.</p>
<h2>What "partial JSON" means here</h2>
<p>A truncated or partial JSON body is not the same as a successful decode with empty Trim. It answers a narrow question: did we receive a complete, parseable document that matches the expected DecodeVinValues shape?</p>
<p>It does <strong>not</strong> authorize:</p>
<ul>
<li>Showing a "decoded" card from a string that throws mid-parse</li>
<li>Guessing closed braces, inventing missing keys, or repairing Results by hand</li>
<li>Promoting a partial object (Make present, ModelYear missing because the stream cut off) into a success state</li>
<li>Reusing the last successful card when the new body failed to parse</li>
<li>Treating HTTP 200 plus a truncated body as "good enough for identity"</li>
</ul>
<p>HTTP status alone is not enough. A 200 with a cut-off Content-Length or a proxy that clipped the body is still a parse failure for UI purposes.</p>
<h2>Parse, then validate shape -- never invent</h2>
<p>Wrap <code>JSON.parse</code> so throwables become an explicit error state. After parse, require the Results array and enough identity keys to justify a card -- or refuse success.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type ParseOutcome =
  | { ok: true; row: Record&lt;string, string&gt; }
  | { ok: false; reason: "truncated_or_invalid_json" | "missing_results" | "incomplete_identity" };

function nonEmpty(raw: string | null | undefined): string | null {
  const t = (raw ?? "").trim();
  if (!t || EMPTY.has(t.toLowerCase())) return null;
  return t;
}

export function parseVpicBody(bodyText: string): ParseOutcome {
  let parsed: unknown;
  try {
    parsed = JSON.parse(bodyText);
  } catch {
    return { ok: false, reason: "truncated_or_invalid_json" };
  }

  if (!parsed || typeof parsed !== "object") {
    return { ok: false, reason: "truncated_or_invalid_json" };
  }

  const results = (parsed as { Results?: unknown }).Results;
  if (!Array.isArray(results) || results.length === 0) {
    return { ok: false, reason: "missing_results" };
  }

  const row = results[0];
  if (!row || typeof row !== "object") {
    return { ok: false, reason: "missing_results" };
  }

  const rec = row as Record&lt;string, string&gt;;
  const make = nonEmpty(rec.Make);
  const model = nonEmpty(rec.Model);
  const year = nonEmpty(rec.ModelYear);
  if (!make || !model || !year) {
    return { ok: false, reason: "incomplete_identity" };
  }

  return { ok: true, row: rec };
}
</code></pre>
<p>Incomplete identity after a valid parse (Make present, Model blank because vPIC left it empty) can be a sparse success elsewhere. Here we refuse full success unless Make, Model, and ModelYear are all sourced. Adjust the bar for a deliberate "partial identity" state -- but never invent the missing pieces.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Catching <code>JSON.parse</code> errors and still rendering a card from regex scraps</li>
<li>Auto-closing truncated JSON with <code>"}]}"</code> so the spinner stops</li>
<li>Showing Make from a half-object while labelling Model "estimating..."</li>
<li>Falling back to the previous VIN's card when the new body is truncated</li>
<li>Logging "decode ok" because status was 200 even when parse failed</li>
</ol>
<p>Refuse those. A truncated body is an error or retry -- not a vehicle.</p>
<pre><code class="language-ts">export type CardState =
  | { kind: "ready"; make: string; model: string; modelYear: string }
  | { kind: "error"; message: string };

const ERROR_MSG = {
  truncated_or_invalid_json:
    "Decode response was truncated or invalid JSON -- not showing a vehicle card",
  missing_results:
    "Decode response had no Results row -- not showing a vehicle card",
  incomplete_identity:
    "Decode response lacked Make, Model, or ModelYear -- not treating as success",
} as const;

export function cardFromBody(bodyText: string): CardState {
  const outcome = parseVpicBody(bodyText);
  if (!outcome.ok) {
    return { kind: "error", message: ERROR_MSG[outcome.reason] };
  }
  return {
    kind: "ready",
    make: String(nonEmpty(outcome.row.Make)),
    model: String(nonEmpty(outcome.row.Model)),
    modelYear: String(nonEmpty(outcome.row.ModelYear)),
  };
}
</code></pre>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"Decode response was truncated or invalid JSON -- try again"</li>
<li>"No Results row in decode response"</li>
<li>"Make, Model, or ModelYear missing -- not showing a complete vehicle card"</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"Decoded (partial)" with invented Model beside a Make scraped from a broken body</li>
<li>Skeleton text that looks like catalog values while parse is still failing</li>
<li>Silent reuse of the last successful decode when the new parse failed</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

const truncated = parseVpicBody('{"Count":1,"Results":[{"Make":"TOYOTA"');
assert.equal(truncated.ok, false);
assert.equal(truncated.reason, "truncated_or_invalid_json");

const sparse = JSON.stringify({
  Count: 1,
  Results: [{ Make: "TOYOTA", Model: "", ModelYear: "2019" }],
});
assert.equal(parseVpicBody(sparse).ok, false);
assert.equal(parseVpicBody(sparse).reason, "incomplete_identity");

const good = JSON.stringify({
  Count: 1,
  Results: [
    { Make: "TOYOTA", Model: "Camry", ModelYear: "2019", Trim: "" },
  ],
});
const card = cardFromBody(good);
assert.equal(card.kind, "ready");
if (card.kind === "ready") {
  assert.equal(card.make, "TOYOTA");
  assert.equal(card.model, "Camry");
  assert.equal(card.modelYear, "2019");
}

const badCard = cardFromBody("{not-json");
assert.equal(badCard.kind, "error");
assert.ok(/truncated or invalid JSON/i.test(
  badCard.kind === "error" ? badCard.message : "",
));
</code></pre>
<p>Review rule: parse modules must not repair JSON or reuse a previous card. Never paint identity from an unparseable body.</p>
<h2>Takeaway</h2>
<p>Truncated or partial vPIC JSON is a failure for vehicle-card purposes, not a prompt to invent Model or repair braces. Parse explicitly, validate Results and identity keys, and show error or retry when the document is incomplete. Your free VIN UI stays trustworthy when success means a real, complete parse -- not a half-invented car from scraps of a broken response.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/handling-truncated-or-partial-vpic-json-without-displaying-half-invented-vehicles-1894">https://dev.to/vin_lookup_8dbd4710f77e9e/handling-truncated-or-partial-vpic-json-without-displaying-half-invented-vehicles-1894</a></p>
]]></content:encoded></item><item><title><![CDATA[Showing ForwardCollisionWarning from vPIC Without Inventing Crash-Avoidance Grades]]></title><description><![CDATA[NHTSA vPIC often returns forward-collision catalog fields on DecodeVinValues-style payloads -- including ForwardCollisionWarning when the pattern includes that token. Those strings are useful on a fre]]></description><link>https://freevinlookup.hashnode.dev/showing-forwardcollisionwarning-from-vpic-without-inventing-crash-avoidance-grades</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/showing-forwardcollisionwarning-from-vpic-without-inventing-crash-avoidance-grades</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[cars]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 13:50:45 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC often returns forward-collision catalog fields on DecodeVinValues-style payloads -- including <code>ForwardCollisionWarning</code> when the pattern includes that token. Those strings are useful on a free VIN decode card. The trap is turning a single sourced field into an AEB claim, IIHS crash-avoidance grade, or "Collision Prevention Package" that the catalog never asserted.</p>
<p>This post is about honest display: show <code>ForwardCollisionWarning</code> when vPIC provides it, refuse crash-avoidance inventing, and allow a clean "not provided" state when the field is empty. Keep forward-collision catalog values separate from automatic emergency braking marketing, star ratings, and OEM suite names. A sourced token is still only a token: it does not prove the feature is present, calibrated, or undamaged on the listed vehicle. (Lane-departure and blind-spot honesty are separate topics; here the focus is ForwardCollisionWarning.)</p>
<h2>What ForwardCollisionWarning is (and is not)</h2>
<p><code>ForwardCollisionWarning</code> is a catalog attribute associated with the VIN decode. It answers a narrow question: which forward-collision warning token did the decode associate with this pattern?</p>
<p>It does <strong>not</strong> answer:</p>
<ul>
<li>Whether the vehicle includes Automatic Emergency Braking (AEB) or pedestrian AEB</li>
<li>Whether IIHS or NHTSA awarded a crash-avoidance or front-crash-prevention grade</li>
<li>Camera vs radar hardware, braking intervention, or alert modality you did not source</li>
<li>Whether the feature works today, was optioned, or was deleted after manufacture</li>
<li>A composite "collision prevention score" built from missing neighboring fields</li>
</ul>
<p>Empty <code>ForwardCollisionWarning</code> does not authorize a default "Standard" badge, and a positive token does not mean "AEB verified." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by model year.</p>
<h2>Normalize empties, do not invent grades</h2>
<p>Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced string when present; do <strong>not</strong> rewrite it into "AEB Standard" or an IIHS-style crash-avoidance grade.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type ForwardCollisionView = {
  value: string | null;
  source: "vpic" | "missing";
};

export function viewForwardCollisionWarning(fields: {
  ForwardCollisionWarning?: string | null;
}): ForwardCollisionView {
  const raw = (fields.ForwardCollisionWarning ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { value: null, source: "missing" };
  }
  return { value: raw, source: "vpic" };
}

export function forwardCollisionLines(view: ForwardCollisionView): string[] {
  if (view.source === "missing") {
    return [
      "Forward collision warning: not provided by vPIC for this VIN",
    ];
  }
  return [
    `Forward collision warning (vPIC): ${view.value}`,
    "Catalog token only -- not an AEB or crash-avoidance grade",
  ];
}
</code></pre>
<p>The footnote matters. Buyers over-read a single forward-collision field as verified AEB. Show the sourced token; if blank, say "not provided" -- no greyed "Standard" and no invented braking claim.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Mapping any non-empty ForwardCollisionWarning into "AEB included"</li>
<li>Inventing IIHS / Euro NCAP / star-equivalent crash-avoidance language from the catalog field</li>
<li>Bundling ForwardCollisionWarning with other ADAS keys into "Full Collision Prevention"</li>
<li>Defaulting blank ForwardCollisionWarning to "Standard" so mid-trim cars look complete</li>
<li>Renaming catalog tokens into OEM suite marketing names you did not source</li>
</ol>
<p>Refuse those. If you show other ADAS catalog fields from separate sourced keys, show each on its own row. Never invent AEB or package language inside the ForwardCollisionWarning mapper.</p>
<pre><code class="language-ts">export function assertNoFcwCrashInvent(moduleSource: string): void {
  const banned = [
    /aeb included/i,
    /automatic emergency braking/i,
    /iihs/i,
    /crash.?avoidance grade/i,
    /front crash prevention/i,
    /full collision prevention/i,
    /euro ncap/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `ForwardCollisionWarning module must not invent crash grades: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  ForwardCollisionWarning?: string | null;
  AdaptiveCruiseControl?: string | null;
  PedestrianAutomaticEmergencyBraking?: string | null;
}): CardRow[] {
  const view = viewForwardCollisionWarning(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({
      label: "Forward collision warning",
      value: "not provided",
    });
  } else {
    out.push({
      label: "Forward collision warning",
      value: String(view.value),
    });
  }

  const acc = (fields.AdaptiveCruiseControl ?? "").trim();
  if (acc &amp;&amp; !EMPTY.has(acc.toLowerCase())) {
    out.push({ label: "Adaptive cruise (vPIC)", value: acc });
  }
  const paeb = (fields.PedestrianAutomaticEmergencyBraking ?? "").trim();
  if (paeb &amp;&amp; !EMPTY.has(paeb.toLowerCase())) {
    out.push({
      label: "Pedestrian AEB catalog (vPIC)",
      value: paeb,
    });
  }
  return out;
}
</code></pre>
<p>Keep neighboring catalog fields on their own labeled rows. Never concatenate them into "Full Collision Prevention Package," and never treat a separate pedestrian-AEB catalog key as proof that ForwardCollisionWarning implies braking.</p>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"Forward collision warning (vPIC): Standard"</li>
<li>"Forward collision warning: not provided by vPIC for this VIN"</li>
<li>A short footnote: catalog token, not an AEB or crash-avoidance grade</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"IIHS Front Crash Prevention Superior -- verified"</li>
<li>"AEB Standard included"</li>
<li>Grey placeholders that look like real forward-collision data when the field was empty</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

assert.equal(viewForwardCollisionWarning({}).source, "missing");
assert.deepEqual(
  forwardCollisionLines(viewForwardCollisionWarning({})),
  ["Forward collision warning: not provided by vPIC for this VIN"],
);

const std = viewForwardCollisionWarning({
  ForwardCollisionWarning: "Standard",
});
assert.equal(std.value, "Standard");
assert.ok(
  forwardCollisionLines(std).some((l) =&gt;
    /not an AEB or crash-avoidance/i.test(l),
  ),
);
assert.ok(
  !forwardCollisionLines(std).some((l) =&gt;
    /iihs|aeb included|collision prevention package/i.test(l),
  ),
);

const rows = cardRows({
  ForwardCollisionWarning: "",
  AdaptiveCruiseControl: "Standard",
  PedestrianAutomaticEmergencyBraking: "Standard",
});
assert.ok(
  rows.some(
    (r) =&gt;
      r.label.includes("Forward collision") &amp;&amp; r.value === "not provided",
  ),
);
assert.ok(rows.some((r) =&gt; r.label.includes("Adaptive cruise")));
assert.ok(rows.some((r) =&gt; r.label.includes("Pedestrian AEB")));
assert.ok(!rows.some((r) =&gt; /iihs|aeb included/i.test(r.value)));
</code></pre>
<p>Review rule: forward-collision modules must not contain AEB-upgrade or crash-avoidance-grade phrases except in forbidding tests. Never upgrade a blank into "Standard."</p>
<h2>Takeaway</h2>
<p><code>ForwardCollisionWarning</code> is a catalog token. Display it with clear sourcing, allow "not provided," and never use it as a key into invented AEB claims, IIHS crash-avoidance grades, or "full collision prevention" bundles. Your free VIN UI stays trustworthy when forward-collision data is either a real vPIC value with a modest footnote -- or absent -- and crash-avoidance marketing lives somewhere else, clearly labeled, or not at all.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/showing-forwardcollisionwarning-from-vpic-without-inventing-crash-avoidance-grades-1dj7">https://dev.to/vin_lookup_8dbd4710f77e9e/showing-forwardcollisionwarning-from-vpic-without-inventing-crash-avoidance-grades-1dj7</a></p>
]]></content:encoded></item><item><title><![CDATA[Displaying SeatBeltType from vPIC Without Inventing Restraint Package Claims]]></title><description><![CDATA[NHTSA vPIC often returns a SeatBeltType field on DecodeVinValues-style payloads: a catalog token describing belt type when the pattern includes it. That string is useful on a free VIN decode card. The]]></description><link>https://freevinlookup.hashnode.dev/displaying-seatbelttype-from-vpic-without-inventing-restraint-package-claims</link><guid isPermaLink="true">https://freevinlookup.hashnode.dev/displaying-seatbelttype-from-vpic-without-inventing-restraint-package-claims</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[cars]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Vin Lookup]]></dc:creator><pubDate>Wed, 07 Oct 2026 13:45:20 GMT</pubDate><content:encoded><![CDATA[<p>NHTSA vPIC often returns a <code>SeatBeltType</code> field on DecodeVinValues-style payloads: a catalog token describing belt type when the pattern includes it. That string is useful on a free VIN decode card. The trap is turning it into a "premium restraint package," brochure safety suite, or IIHS-equivalent claim that a seat-belt catalog value never asserted.</p>
<p>This post is about honest display: show <code>SeatBeltType</code> when vPIC provides it, refuse restraint-package inventing, and allow a clean "not provided" state when the field is empty. Keep seat-belt catalog values separate from airbag locations, pretensioner marketing, and crash-test grades. A sourced token is still only a token: it does not prove belts are installed correctly, unmodified, or undamaged on the listed vehicle. (Airbag location honesty is a separate topic; here the focus is SeatBeltType alone.)</p>
<h2>What SeatBeltType is (and is not)</h2>
<p><code>SeatBeltType</code> is a catalog attribute associated with the VIN decode. It answers a narrow question: which seat-belt type token did the decode associate with this pattern?</p>
<p>It does <strong>not</strong> answer:</p>
<ul>
<li>Whether the vehicle earned an IIHS or NHTSA star rating</li>
<li>Whether a dealer "Premium Restraint Package" or OEM suite name is equipped</li>
<li>Pretensioner health, load-limiter status, or aftermarket belt changes</li>
<li>Whether belts work today or were replaced after an incident</li>
<li>A composite "restraint completeness" score built from missing neighboring fields</li>
</ul>
<p>Empty <code>SeatBeltType</code> does not authorize a default "3-Point" badge, and a positive token does not mean "premium restraint package verified." Do not fill gaps from Make/Model/Year folklore or a chart keyed only by model year.</p>
<h2>Normalize empties, do not invent packages</h2>
<p>Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Keep a sourced string when present; do <strong>not</strong> rewrite it into "Premium Restraint Package" or an IIHS-style grade.</p>
<pre><code class="language-ts">const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type SeatBeltTypeView = {
  value: string | null;
  source: "vpic" | "missing";
};

export function viewSeatBeltType(fields: {
  SeatBeltType?: string | null;
}): SeatBeltTypeView {
  const raw = (fields.SeatBeltType ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { value: null, source: "missing" };
  }
  return { value: raw, source: "vpic" };
}

export function seatBeltTypeLines(view: SeatBeltTypeView): string[] {
  if (view.source === "missing") {
    return [
      "Seat belt type: not provided by vPIC for this VIN",
    ];
  }
  return [
    `Seat belt type (vPIC): ${view.value}`,
    "Catalog token only -- not a restraint package or crash-grade claim",
  ];
}
</code></pre>
<p>The footnote matters. Buyers over-read a belt token as a verified safety suite. Show the sourced value; if blank, say "not provided" -- no greyed "3-Point Standard" and no invented package name.</p>
<h2>Forbidden upgrades</h2>
<p>Product pressure often asks for:</p>
<ol>
<li>Mapping any non-empty SeatBeltType into "Premium Restraint Package"</li>
<li>Inventing IIHS / Euro NCAP / star-equivalent language from belt catalog fields</li>
<li>Bundling SeatBeltType with airbag location fields into "Full Restraint Suite"</li>
<li>Defaulting blank SeatBeltType to "3-Point" so mid-trim cars look complete</li>
<li>Renaming catalog tokens into OEM restraint marketing names you did not source</li>
</ol>
<p>Refuse those. If you show airbag or other restraint catalog fields from separate sourced keys, show each on its own row. Never invent a package name inside the SeatBeltType mapper.</p>
<pre><code class="language-ts">export function assertNoRestraintPackageInvent(
  moduleSource: string,
): void {
  const banned = [
    /premium restraint/i,
    /restraint package/i,
    /iihs/i,
    /5-star equivalent/i,
    /full restraint suite/i,
    /euro ncap/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `SeatBeltType module must not invent restraint packages: ${re}`,
      );
    }
  }
}

export type CardRow = { label: string; value: string };

export function cardRows(fields: {
  SeatBeltType?: string | null;
  AirBagLocFront?: string | null;
  AirBagLocSide?: string | null;
}): CardRow[] {
  const view = viewSeatBeltType(fields);
  const out: CardRow[] = [];
  if (view.source === "missing") {
    out.push({
      label: "Seat belt type",
      value: "not provided",
    });
  } else {
    out.push({
      label: "Seat belt type",
      value: String(view.value),
    });
  }

  const front = (fields.AirBagLocFront ?? "").trim();
  if (front &amp;&amp; !EMPTY.has(front.toLowerCase())) {
    out.push({ label: "Front airbag loc (vPIC)", value: front });
  }
  const side = (fields.AirBagLocSide ?? "").trim();
  if (side &amp;&amp; !EMPTY.has(side.toLowerCase())) {
    out.push({ label: "Side airbag loc (vPIC)", value: side });
  }
  return out;
}
</code></pre>
<p>Keep airbag locations on their own labeled rows. Never concatenate belts with airbags into "Premium Restraint Package."</p>
<h2>UI copy that stays honest</h2>
<p>Prefer:</p>
<ul>
<li>"Seat belt type (vPIC): Manual"</li>
<li>"Seat belt type: not provided by vPIC for this VIN"</li>
<li>A short footnote: catalog token, not a restraint package claim</li>
</ul>
<p>Avoid:</p>
<ul>
<li>"Premium Restraint Package verified"</li>
<li>"IIHS Good -- belt system complete"</li>
<li>Grey placeholders that look like real belt data when the field was empty</li>
</ul>
<h2>Quick checks</h2>
<pre><code class="language-ts">import assert from "node:assert/strict";

assert.equal(viewSeatBeltType({}).source, "missing");
assert.deepEqual(
  seatBeltTypeLines(viewSeatBeltType({})),
  ["Seat belt type: not provided by vPIC for this VIN"],
);

const manual = viewSeatBeltType({ SeatBeltType: "Manual" });
assert.equal(manual.value, "Manual");
assert.ok(
  seatBeltTypeLines(manual).some((l) =&gt;
    /not a restraint package/i.test(l),
  ),
);
assert.ok(
  !seatBeltTypeLines(manual).some((l) =&gt;
    /premium|iihs|full restraint/i.test(l),
  ),
);

const rows = cardRows({
  SeatBeltType: "",
  AirBagLocFront: "1st Row (Driver and Passenger)",
  AirBagLocSide: "1st Row (Driver and Passenger)",
});
assert.ok(
  rows.some(
    (r) =&gt;
      r.label.includes("Seat belt") &amp;&amp; r.value === "not provided",
  ),
);
assert.ok(rows.some((r) =&gt; r.label.includes("Front airbag")));
assert.ok(rows.some((r) =&gt; r.label.includes("Side airbag")));
assert.ok(!rows.some((r) =&gt; /premium restraint|iihs/i.test(r.value)));
</code></pre>
<p>Review rule: seat-belt modules must not contain restraint-package marketing phrases except in forbidding tests. Never upgrade a blank into "3-Point."</p>
<h2>Takeaway</h2>
<p><code>SeatBeltType</code> is a catalog token. Display it with clear sourcing, allow "not provided," and never use it as a key into invented premium restraint packages, crash grades, or bundled airbag suites. Your free VIN UI stays trustworthy when seat-belt data is either a real vPIC value with a modest footnote -- or absent -- and restraint marketing lives somewhere else, clearly labeled, or not at all.</p>
<p>I maintain <a href="https://free-vin-lookup.com/">VIN Lookup</a>, a free VIN decode based on NHTSA data.</p>
<p>Originally published on DEV: <a href="https://dev.to/vin_lookup_8dbd4710f77e9e/displaying-seatbelttype-from-vpic-without-inventing-restraint-package-claims-5dnc">https://dev.to/vin_lookup_8dbd4710f77e9e/displaying-seatbelttype-from-vpic-without-inventing-restraint-package-claims-5dnc</a></p>
]]></content:encoded></item></channel></rss>