Skip to main content
Running out of credits mid-batch is disruptive, especially if your system doesn’t notice until the error rate spikes. Catch it early, degrade gracefully, and route around it.

Early detection

Poll your credit balance before starting work that will consume a lot:
async function ensureCredits(needed: number) {
  const credits = await client.getCredits();
  if (credits.balance < needed) {
    throw new Error(
      `Insufficient credits: need ${needed}, have ${credits.balance}. ` +
        `Resets at ${credits.resetAt}.`,
    );
  }
}

await ensureCredits(estimateBatchCost(urls));
const result = await client.read({ urls });
This isn’t foolproof (other requests on the same workspace can consume credits between your check and your actual /v1/read call), but for big batches it catches the obvious “you’re nowhere near enough” case.

Catching the 402

When /v1/read returns insufficient_credits:
import { InsufficientCreditsError } from "@vakra-dev/reader-js";

try {
  await client.read({ url });
} catch (err) {
  if (err instanceof InsufficientCreditsError) {
    console.error(`Need ${err.required}, have ${err.available}`);
    console.error(`Resets at ${err.resetAt}`);
    await pauseWorker();
    await alertOps("reader-credits-exhausted", {
      available: err.available,
      resetAt: err.resetAt,
    });
    return;
  }
  throw err;
}

The auto mode escalation edge case

The pre-flight credit check for auto mode is optimistic (1 credit per page, not 3). If auto escalates to stealth mid-request, your balance can briefly go negative, and the next request fails with insufficient_credits even though the one that caused the overdraft succeeded. This is intentional (it mirrors Stripe’s metered billing), but it means your error handler should be able to tell the difference between “I genuinely have zero credits” and “I have zero credits because the last request escalated”. In both cases the fix is the same: top up, wait for reset, or route to a lower-cost path.

Low-credit webhook

Subscribe to credit.low to get notified before you run out:
await client.request("POST", "/v1/webhooks", {
  url: "https://your-app.example.com/hooks/reader",
  name: "Credit alerts",
  events: ["credit.low"],
  secret: process.env.READER_WEBHOOK_SECRET,
});
Reader fires this event when your balance drops below 10% of your monthly limit. That gives you runway to:
  • Alert an operator
  • Pause background workers
  • Switch to a higher tier automatically (if you have a billing API)
  • Queue further requests until the next reset

Graceful degradation strategies

When you’re out of credits, what should your app actually do?
  • Cached responses only. Set cache: true (the default) and accept that anything not already cached is unavailable until reset. Cache hits are free.
  • Queue and defer. Accept user requests, queue the scrapes, run them when credits come back.
  • Fail visibly to users. Better than silent corruption; show a “service degraded” banner.
  • Switch to a cheaper mode. Temporarily force proxyMode: "standard" instead of auto to cut the cost by up to 3x per page.

Credit budget per worker

For sustained workloads, budget your credits like you budget memory: cap each worker’s daily draw and alert when it exceeds.
const DAILY_BUDGET = 1000;
let dailySpend = 0;

async function scrapeWithBudget(url: string) {
  if (dailySpend >= DAILY_BUDGET) {
    throw new Error("Daily budget exhausted");
  }

  const result = await client.read({ url });
  // The actual cost is 1 or 3 depending on the resolved mode
  const cost = result.data.metadata?.proxyMode === "stealth" ? 3 : 1;
  dailySpend += cost;

  return result;
}

Next