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: graceful degradation -- 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."
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.
Graceful degrade vs load-shed, circuits, concurrency, retry budget
- Load shed -- refuse or drop requests when the edge is overloaded
- Circuit breaker -- stop calling an unhealthy dependency globally for a cool-down
- Concurrency / retry budget -- cap in-flight calls and per-VIN attempts
- Graceful degrade -- keep serving the critical decode path; secondary enrichers become optional under a remaining-time budget
Degrade is about partial success for one request. Shed is about protecting the fleet. You can degrade without shedding, and shed without offering a partial card.
Priority tiers and a remaining-time budget
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.
export type EnricherId =
| "vpic-primary"
| "recall-snippet"
| "photo-hint"
| "analytics-ping";
export type EnricherResult =
| { id: EnricherId; status: "ok"; data: Record<string, string> }
| { 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) => Promise<Record<string, string>>;
};
export async function runWithGracefulDegrade(
vin: string,
enrichers: Enricher[],
deadline: Deadline,
): Promise<EnricherResult[]> {
const out: EnricherResult[] = [];
for (const e of enrichers) {
const left = remainingMs(deadline);
if (!e.critical && left < e.minMs) {
out.push({ id: e.id, status: "skipped", reason: "deadline" });
continue;
}
const ac = new AbortController();
const timer = setTimeout(
() => 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" && 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;
}
Primary failure fails the request (or returns a clear error). Secondary skips leave the core Make/Model/Year rows intact when primary succeeded.
UI honesty under degrade
Prefer:
- Core vPIC rows from a successful primary
- "Recall snippet: skipped (deadline) -- not provided this request"
- A banner: "Partial decode -- secondary enrichment deferred"
Avoid:
- Grey fake recall counts or "no recalls" invented on skip
- Hiding that photo-hint never ran
- Relabeling a degraded card as "full enrichment complete"
Forbidden upgrades
- Inventing secondary enrichment fields when skipped for deadline
- Blocking the whole card on non-critical enrichers after primary succeeded
- Treating graceful degrade as a license to ignore concurrency / retry budgets
- Opening the circuit solely because one enricher skipped (different tool)
- Shedding healthy primary decodes when you only needed to skip analytics
Refuse those. Partial + labeled beats complete + fake.
Quick checks
import assert from "node:assert/strict";
const enrichers: Enricher[] = [
{
id: "vpic-primary",
critical: true,
minMs: 50,
run: async () => ({ Make: "HONDA", Model: "Accord" }),
},
{
id: "recall-snippet",
critical: false,
minMs: 80,
run: async (_v, signal) => {
await new Promise((r, j) => {
const t = setTimeout(r, 200);
signal.addEventListener("abort", () => {
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) => r.id === "vpic-primary" && r.status === "ok"));
assert.ok(
results.some(
(r) => r.id === "recall-snippet" && r.status === "skipped",
),
);
const rows = cardFromResults(results);
assert.ok(rows.some((r) => r.label === "Make" && r.value === "HONDA"));
assert.ok(
rows.some((r) => /skipped|not provided this request/i.test(r.value)),
);
assert.ok(!rows.some((r) => r.label === "recalls" && r.value === "0"));
Review rule: degrade modules must preserve primary success and must not invent secondary fields on skip.
Takeaway
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.
I maintain VIN Lookup, a free VIN decode based on NHTSA data.
Top comments (0)