Caching and credit budgets

Every API call has a price. Learn to see it, stop paying twice for the same answer, and make a bulk job check its budget before it starts instead of after it fails.

Up to now every call has been treated as free: write the helper, call it whenever you need an answer. On a metered API that habit is how a weekend side project burns a month's allowance on Saturday night. A page that looks up the visitor's country on every load, a loop that validates the same email list each time the script runs, a retry that fires on a credit-exhaustion error: each one is cheap per call and expensive in aggregate.

This lesson is about the arithmetic. Know what a call costs, avoid making the same call twice, and check the balance before committing to a big job.

What a call costs

On APIVerve each API has a fixed credit cost per successful call, set in its definition and shown on its reference page. Most cost one or two credits; APIs that do more work per request cost more. The APIs used in this course:

APICredits per callWhat drives the cost
Number to Words1Pure computation
Country Lookup2Reference data lookup
Exchange Rate2Live rates
Dictionary, World Time2Lookup
IP Lookup10Geolocation database
Email Validator20Several live DNS checks per address

The spread is the point. Ten thousand page views that each call Number to Words cost 10,000 credits. The same traffic calling Email Validator costs 200,000. Before you put any call in a hot path, multiply its cost by how often that path runs.

Two billing rules change how you write error handling:

  • Failed calls are not billed. A 400, a rejected key or a blocked call costs nothing, because it never reached the source that charges.
  • An empty successful answer is billed. A lookup that ran and found nothing did the work you asked for. "No result" is still a result.

Neither rule makes retrying a 4xx free in practice: it still costs you time, rate-limit headroom, and eventually the repeated-failure throttle from lesson three.

Cache what does not change

A cached response costs zero credits and answers in microseconds. The API does not cache for you, and it sends no Cache-Control header that would let a proxy do it, because a cached answer you did not ask for is a wrong answer on the endpoints that change by the second. Caching is a decision you make per endpoint, based on how fast the underlying data actually moves.

Kind of dataReasonable cache lifetimeExample
Country, currency, timezone, ISO codesWeeksCountry Lookup for JP
Pure functions of the inputIndefinitelyNumber to Words for 1999
Geocoding, DNS, WHOIS, company recordsHours to a dayIP Lookup
Weather, exchange rates, market dataMinutesExchange Rate USD to EUR
Validation, current time, random outputDo not cacheEmail Validator, World Time

The last row deserves a concrete example. Validating [email protected] twice a few minutes apart returned the same verdict but different mxRecord values (mxa-... and then mxb-...), because the domain publishes several mail servers and DNS rotates through them. A validation result describes the world at the moment of the call. Caching it for a day means trusting yesterday's DNS. And anything time-based or random is wrong the instant it is cached.

Try it liveCountry Lookup2 credits
GEThttps://api.apiverve.com/v1/countrylookup
curl "https://api.apiverve.com/v1/countrylookup?country=JP" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

Nothing in data.countries[0] will change for years. This is the ideal thing to cache.

A TTL cache in JavaScript

A Map with an expiry time per entry is enough for a single process. This builds on the apiverve() helper from lesson three.

cache.mjs
import { apiverve } from './apiverve-retry.mjs';

const cache = new Map();
const MAX_ENTRIES = 1000;

function cacheKey(id, params) {
  const sorted = Object.keys(params).sort().map((k) => [k, String(params[k])]);
  return `${id}?${new URLSearchParams(sorted)}`;
}

export async function cached(id, params, ttlMs) {
  const key = cacheKey(id, params);
  const hit = cache.get(key);
  if (hit && hit.expires > Date.now()) return hit.value;

  const value = apiverve(id, params);           // store the Promise itself
  cache.set(key, { value, expires: Date.now() + ttlMs });
  if (cache.size > MAX_ENTRIES) cache.delete(cache.keys().next().value);

  try {
    return await value;
  } catch (err) {
    cache.delete(key);                          // never cache a failure
    throw err;
  }
}

Usage:

const WEEK = 7 * 24 * 60 * 60 * 1000;
const a = await cached('countrylookup', { country: 'JP' }, WEEK);  // 2 credits
const b = await cached('countrylookup', { country: 'JP' }, WEEK);  // 0 credits
console.log(a.countries[0].capital[0]);                            // "Tokyo"

Each line in cached() is there to prevent a specific bug:

  • The key is built from sorted parameters. {from, to} and {to, from} are the same question and should share one entry. Without sorting, you pay twice for an identical lookup.
  • The Promise is cached, not the resolved value. If twenty requests for JP arrive in the same millisecond on a cold cache, they all find the same pending Promise and share one API call. Caching only after the await would fire twenty calls and pay forty credits. This is called a cache stampede, and it happens on every server restart.
  • Failures are removed. A cached rejection would replay a transient 503 to every caller until the entry expired.
  • The size is capped. A Map iterates in insertion order, so deleting the first key evicts the oldest entry. Without a cap, a cache keyed on user input is a memory leak with extra steps.

Normalise inputs before they reach the key when the API treats them the same, such as trimming whitespace or upper-casing currency codes. Do it only when you are sure the API really does treat them the same; normalising away a real difference serves one user another user's answer.

Caching in Python

For pure lookups, the standard library already has a cache:

lookups.py
from functools import lru_cache
from apiverve_retry import apiverve


@lru_cache(maxsize=512)
def number_words(n):
    return apiverve("numbertowords", {"number": n})["words"]


print(number_words(1999))   # calls the API
print(number_words(1999))   # served from memory
print(number_words.cache_info())

lru_cache is the right tool for exactly this case and the wrong one for most others. It keys on the function's arguments, so they must be hashable: you cannot pass a params dict, which is why the wrapper takes a single number. It has no expiry, so it suits data that never changes for the life of the process. It does not cache exceptions, which is good. And it hands every caller the same object: if one caller mutates a returned dict, every later caller sees the mutation. Returning a string, as above, avoids that.

When data does go stale, use a dict with timestamps:

ttl_cache.py
import time
from apiverve_retry import apiverve

_cache = {}


def cached(api_id, params, ttl_seconds):
    key = (api_id, tuple(sorted((k, str(v)) for k, v in params.items())))
    hit = _cache.get(key)
    if hit and hit[1] > time.monotonic():
        return hit[0]
    data = apiverve(api_id, params)       # raises on failure, so nothing is cached
    _cache[key] = (data, time.monotonic() + ttl_seconds)
    return data


rate = cached("exchangerate", {"currency1": "USD", "currency2": "EUR"}, ttl_seconds=300)
print(float(rate["exchangeRate"]))

Use time.monotonic() rather than time.time() for expiry: the wall clock can jump when the system syncs its time, and a backwards jump would keep stale entries alive. Note the float() on exchangeRate: the API returns rates as strings to preserve precision, and string arithmetic in Python fails loudly where JavaScript would coerce quietly.

Try it liveExchange Rate2 credits
GEThttps://api.apiverve.com/v1/exchangerate
curl "https://api.apiverve.com/v1/exchangerate?currency1=USD&currency2=EUR" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

data.exchangeRate is a string, and data.lastUpdated shows how fresh it is. Five minutes of caching is reasonable.

Check yourself

Twenty requests for the same uncached country arrive at once. Your cache stores the resolved data only after await apiverve(...) finishes. How many API calls are made?

Answer the check above to continue.

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales