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:
| API | Credits per call | What drives the cost |
|---|---|---|
| Number to Words | 1 | Pure computation |
| Country Lookup | 2 | Reference data lookup |
| Exchange Rate | 2 | Live rates |
| Dictionary, World Time | 2 | Lookup |
| IP Lookup | 10 | Geolocation database |
| Email Validator | 20 | Several 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 data | Reasonable cache lifetime | Example |
|---|---|---|
| Country, currency, timezone, ISO codes | Weeks | Country Lookup for JP |
| Pure functions of the input | Indefinitely | Number to Words for 1999 |
| Geocoding, DNS, WHOIS, company records | Hours to a day | IP Lookup |
| Weather, exchange rates, market data | Minutes | Exchange Rate USD to EUR |
| Validation, current time, random output | Do not cache | Email 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.
curl "https://api.apiverve.com/v1/countrylookup?country=JP" \
-H "x-api-key: YOUR_API_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.
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
JParrive in the same millisecond on a cold cache, they all find the same pending Promise and share one API call. Caching only after theawaitwould 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
503to every caller until the entry expired. - The size is capped. A
Mapiterates 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:
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:
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.
curl "https://api.apiverve.com/v1/exchangerate?currency1=USD¤cy2=EUR" \
-H "x-api-key: YOUR_API_KEY"data.exchangeRate is a string, and data.lastUpdated shows how fresh it is. Five minutes of caching is reasonable.
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.
The course in one helper
Across five lessons the same small function picked up everything a production client needs: the key from the environment, encoded parameters, both layers of success checked, a deadline, retries only where they help, a hard stop on credit exhaustion, a proxy so browsers never see the key, and a cache plus a budget so you only pay for answers you actually need.
The next step is to use it on something real. The projects course builds complete small applications on these patterns: Build real projects.