Setting Per-Request Time Budgets for NHTSA Calls in Serverless
Serverless VIN decode handlers die in two ways: the platform kills the whole invocation, or your fetch to NHTSA vPIC hangs until the user gives up. Platform limits are blunt (often 10s, 30s, or 60s depending on the host)
Serverless VIN decode handlers die in two ways: the platform kills the whole invocation, or your fetch to NHTSA vPIC hangs until the user gives up. Platform limits are blunt (often 10s, 30s, or 60s depending on the host). What you need for a free decode product is a per-request time budget you own: how long this one VIN lookup may wait on the network before you return a controlled failure.
This post is about budgets, not UI copy. For timeout messaging patterns see complementary UX notes; here we focus on TypeScript that cancels outbound NHTSA work before the function wall clock expires.
Why a budget is not the same as fetch timeout
Many stacks let you set a single HTTP timeout. That is necessary and insufficient:
- Your handler may validate the VIN, check a cache, coalesce duplicates, then call DecodeVinValues
- Cold start plus DNS plus TLS already ate part of the platform limit
- Retries without a remaining-budget check can exceed the invocation ceiling even when each attempt looks "short"
A budget answers: how many milliseconds may this request still spend waiting on NHTSA? Everything else (local validation, cache hits) should leave that clock alone or shrink it deliberately.
Model remaining time explicitly
export type Budget = {
deadlineMs: number;
};
export function startBudget(maxWaitMs: number, now = Date.now()): Budget {
return { deadlineMs: now + maxWaitMs };
}
export function remainingMs(budget: Budget, now = Date.now()): number {
return Math.max(0, budget.deadlineMs - now);
}
export function assertBudget(budget: Budget, minMs = 50): void {
if (remainingMs(budget) < minMs) {
throw new Error("BUDGET_EXCEEDED");
}
}
Pick maxWaitMs below the platform max. On a 10s Cloudflare Worker or Vercel function, a 2500-4000ms NHTSA budget is often enough for one attempt plus a short retry window, while leaving headroom for JSON parse and response assembly.
Wire AbortSignal to the remaining budget
AbortController is the right cancellation tool. Derive a signal from remaining time so a late retry cannot outlive the budget.
export function signalForBudget(budget: Budget): AbortSignal {
const ms = remainingMs(budget);
if (ms <= 0) {
const c = new AbortController();
c.abort();
return c.signal;
}
return AbortSignal.timeout(ms);
}
export async function decodeVinWithBudget(
vin: string,
budget: Budget,
): Promise<unknown> {
assertBudget(budget, 100);
const url =
"https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/" +
encodeURIComponent(vin) +
"?format=json";
const res = await fetch(url, {
method: "GET",
signal: signalForBudget(budget),
headers: { Accept: "application/json" },
});
if (!res.ok) {
throw new Error(`NHTSA_HTTP_${res.status}`);
}
return res.json();
}
AbortSignal.timeout (or a polyfill that aborts after ms) ties the HTTP call to the same deadline your handler respects. Do not nest a second fixed 8s timeout that ignores how much budget remains.
Reserve time for retries
If you retry on 503 or network reset, subtract a floor for the next attempt before you sleep.
export async function decodeWithOneRetry(
vin: string,
budget: Budget,
): Promise<unknown> {
try {
return await decodeVinWithBudget(vin, budget);
} catch (err) {
const retryFloorMs = 800;
assertBudget(budget, retryFloorMs);
// optional short jittered delay that still fits remainingMs
const delay = Math.min(200, remainingMs(budget) - retryFloorMs);
if (delay > 0) {
await new Promise((r) => setTimeout(r, delay));
}
return decodeVinWithBudget(vin, budget);
}
}
Never schedule a 1s backoff when only 400ms remain. That turns a clean budget failure into a platform kill with a worse log shape.
Map budgets to serverless hosts
| Host constraint | Suggested NHTSA budget | Notes |
|---|---|---|
| 10s wall clock | 2.5-3.5s | One attempt; retry only if remaining > 1s |
| 30s wall clock | 4-6s | Room for one jittered retry |
| Edge isolate with CPU limits | Prefer cache-first | Budget still caps outbound wait |
Keep the budget constant in config (NHTSA_BUDGET_MS), not hard-coded next to the fetch. Ops can tighten it during NHTSA slowdowns without redeploying UI strings.
What to return when the budget trips
Throw a typed error your API route maps to HTTP 504 or 503 with a stable machine code such as DECODE_BUDGET_EXCEEDED. Do not pretend the VIN is invalid. Do not return an empty make/model object that looks like a successful sparse decode. Cache misses and budget kills are different events; log them separately so you can tell "vPIC slow" from "bad VIN".
Takeaway
In serverless VIN decode paths, own a per-request millisecond budget, derive AbortSignal from remaining time, and refuse retries that cannot finish inside that window. Platform timeouts are a backstop, not a product strategy. Cap NHTSA wait deliberately and your free decode stays predictable when the public API gets slow.
I maintain VIN Lookup, a free VIN decode based on NHTSA data.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.