Caching Strategies for IP Geolocation APIs: TTLs, Keys, and Invalidation

How to cache IP geolocation responses correctly — in-memory, Redis, CDN edge — with TTL guidance, invalidation patterns, and the math of why caching is the single biggest cost-cutter for IP enrichment.

Caching Strategies for IP Geolocation APIs: TTLs, Keys, and Invalidation

If you’re calling an IP geolocation API on every request, you’re doing it wrong. The geo of an IP doesn’t change minute to minute, but most naive integrations re-query the same IP hundreds of times an hour. Caching turns “calling the API on every request” into “calling the API once per IP per minute,” which usually reduces API spend by 95%+ and shaves real latency off every cached page load.

This post is the comprehensive guide: when to cache, where to cache, how long to cache, what to invalidate, and the numbers behind the recommendations.

The Case for Caching

Take a typical site with 100,000 unique daily visitors and 5 page views per visitor on average. That’s 500,000 page requests per day. Without caching, that’s 500,000 API calls.

With a 5-minute cache, the same traffic generates maybe 50,000–100,000 API calls — an 80–90% reduction. With a longer cache (1 hour), it drops further, often to <10% of uncached.

The math:

  • Each cache hit: ~0.1ms (in-memory) to 2ms (Redis), local to your service.
  • Each cache miss: ~10–100ms, includes network round-trip to the API.
  • Cost per call: typically pennies per thousand, but it compounds.

For high-traffic services, caching is the difference between a $50/month API bill and a $5,000/month API bill. For latency-sensitive paths, it’s the difference between a page that feels snappy and one that feels laggy.

What’s Cacheable

IP-to-geo data is inherently cacheable for several reasons:

  • It rarely changes. An IP’s country might shift if the IP is reassigned to a different organization, but that’s days or weeks of stability — not minute-to-minute volatility.
  • It’s not personal in a way caching exposes. Caching 8.8.8.8 → US doesn’t leak any user-specific data; many users share the same IP behind NAT/CGNAT.
  • It’s the same answer for the same input. Pure function with deterministic output.

Compare to caching things like user profiles (which change on every edit), pricing data (which changes with market conditions), or session data (which is per-user). IP geo is the easiest possible thing to cache.

TTL Recommendations

How long should you cache? It depends on your tolerance for staleness vs your cost optimization goals. Some defaults:

60 seconds (1 minute)

When to use: Real-time fraud detection where you want the freshest possible data on suspicious activity.

Trade-off: Higher API cost; minimal staleness risk.

300 seconds (5 minutes)

When to use: Most production web applications. Balances freshness and cost reasonably.

Trade-off: Sensible default. Reduces API calls by ~80–95% for typical traffic.

3600 seconds (1 hour)

When to use: High-traffic services where API cost is significant; the application doesn’t require minute-by-minute geo freshness.

Trade-off: Very few API calls; a user whose IP changed country (e.g., they hopped on a VPN) will see stale data for up to an hour.

86400 seconds (24 hours)

When to use: Analytics or reporting where the IP-to-geo mapping is being used to enrich logs after the fact, and same-day accuracy is fine.

Trade-off: Almost zero API calls; staleness is rarely a problem since you’re not making per-request decisions on the data.

7+ days

When to use: Almost never, except for static datasets you control and refresh on a schedule.

Trade-off: IPs do move between countries occasionally (reassignment to a new ISP, new BGP announcements). After a week, you’ll see some incorrect classifications.

A good starting point: 5 minutes for general-purpose web apps. Tune up or down based on your specific cost/freshness needs.

Where to Cache

1. In-Process Cache (LRU + TTL)

The fastest possible cache: a dictionary in memory.

Pros:

  • Sub-millisecond hits.
  • No external dependencies.
  • No serialization overhead.

Cons:

  • Per-instance (each app server has its own cache; cache misses on instance churn).
  • Memory-bound (eviction policy must be sane).
  • Lost on restart.

When to use: Single-instance applications, edge functions with limited storage options, anything where the cache hit rate is more important than cross-instance consistency.

How to size: max entries should be roughly the number of unique IPs you expect to see in a TTL window. For a site with 10K unique IPs in 5 minutes, set max=10000, ttl=300.

2. Redis (or Memcached)

External in-memory store, shared across all app servers.

Pros:

  • Cross-instance consistency.
  • Survives app restarts (depending on persistence settings).
  • Can handle massive entry counts (millions of IPs at low memory cost).
  • Native TTL support.

Cons:

  • Small network hop per lookup (1–5ms typical).
  • Operational overhead (you have to run Redis).
  • Serialization cost (typically JSON).

When to use: Multi-instance services. The standard pattern for any production web app or API. The cross-instance consistency is significant — without it, a user hitting two different app servers gets two cache misses instead of one.

3. CDN / Edge Cache

If you’re behind a CDN, you can cache geo lookups at the edge — letting the CDN do the lookup and inject it as a header into the request.

Pros:

  • Zero added latency (the CDN’s edge POP is already the user’s nearest hop).
  • Massive scale (CDN caches handle billions of requests).
  • Often “free” — included in CDN bills.

Cons:

  • Less control over TTL and invalidation.
  • Limited to what the CDN provides (Cloudflare gives country and basic data; other CDNs less).
  • Doesn’t work for non-HTTP traffic.

When to use: Web applications already on a CDN, especially for country-level data. Augment with API calls when you need richer fields.

4. Database Cache

Storing geo data in your own DB tables.

Pros:

  • Permanent (no TTL eviction unless you implement it).
  • Queryable (you can join geo data with other tables).
  • Useful for batch enrichment (look up geo for unique IPs, store the result).

Cons:

  • Slow compared to memory caches.
  • Refresh logic is on you.
  • Bloat over time if not managed.

When to use: Analytics rollups, log enrichment, batch jobs. Not for hot-path request handling.

Layered Caching

The most efficient pattern combines layers:

Request → check in-process cache (1ms)
       → on miss, check Redis (5ms)
       → on miss, call API (50ms)
       → on success, write back to both caches

For an app with 10K unique IPs/hour, this typically gives:

  • ~95% in-process cache hit rate (sub-ms)
  • ~4% Redis cache hit rate (low-ms)
  • ~1% API call rate

The numbers compound. A 1% API call rate vs 100% means 99x fewer API calls, which is 99% lower cost and 99% less latency-impact on average.

Cache Keys

Simple: the IP is the key.

geo:8.8.8.8
geo:2606:4700:20::ac43:4a8a

A few notes:

  • Use a prefix (geo:) so you can flush all geo entries without touching other Redis data.
  • Canonicalize the IP if your input format varies. 127.0.0.1 and ::ffff:127.0.0.1 are the same address — don’t cache them separately.
  • Don’t include API key or other variables in the key — the geo response for an IP is the same regardless of who’s asking.

What to Cache (the Response)

You can either cache the entire API response or just the fields you actually use. Trade-offs:

Cache the whole response

{
  "ip": "8.8.8.8",
  "type": "v4",
  "is_eu": false,
  "continent": { "name": "North America", ... },
  ...
}

Pros: No re-fetch needed if you decide to use a new field. Simpler code.

Cons: Larger cache footprint (a typical response is 2–5KB).

Cache a slimmed-down version

{
  "country": "US",
  "city": "Mountain View",
  "asn": 15169,
  "isVpn": false
}

Pros: Smaller footprint, much faster serialization for high-throughput services.

Cons: If you start needing a new field, you have to re-fetch and re-cache.

For most apps, cache the whole response. Memory is cheap; flexibility is valuable. Only slim down if you’re operating at a scale where the response size genuinely matters.

Negative Caching

What about IPs that fail to look up — private ranges, malformed inputs, API errors?

Yes, cache them too. Otherwise every request from a private IP triggers a new (failing) API call. A short TTL (60 seconds) for negative results prevents the same failure from being retried thousands of times per minute.

const result = await convertIP(ip)
if (result.success) {
    cache.set(ip, result.data, 300)  // 5 min positive cache
} else {
    cache.set(ip, null, 60)  // 1 min negative cache
}

For the SDK-level cache, this is handled automatically. For roll-your-own caching, remember the negative case.

Invalidation

The general advice (“cache invalidation is hard”) doesn’t really apply to IP geo. Some specifics:

Time-based invalidation (TTL)

The default. Set a TTL, let the cache evict itself. Works for ~99% of cases.

Manual invalidation

You might want to flush all geo data after:

  • Switching API providers (the new provider may return slightly different data).
  • A known IP-range reassignment that affects your users.
  • A bug fix in your enrichment logic.

A simple flush command (redis-cli --scan --pattern 'geo:*' | xargs redis-cli DEL) handles this. Don’t overthink it.

IP-specific invalidation

Very rarely useful. If you know a specific IP has changed country, you can DEL geo:1.2.3.4. In practice the TTL will handle this within minutes anyway.

Bulk Lookups with Cache Awareness

When you have many IPs to look up (e.g., enriching a log file with millions of rows), the smart pattern is:

  1. Get unique IPs. Don’t look up the same IP 1000 times.
  2. Check the cache first. Pull whatever is already cached.
  3. Batch-lookup the misses. Use convertIPs([...]) for the uncached IPs.
  4. Write the new results back to the cache.
  5. Apply the results to your original dataset.

In Python:

# Get unique IPs
unique_ips = set(log_df['ip'].unique())

# Check cache
cached_results = {}
uncached_ips = []
for ip in unique_ips:
    cached = redis.get(f'geo:{ip}')
    if cached:
        cached_results[ip] = json.loads(cached)
    else:
        uncached_ips.append(ip)

# Batch-fetch the misses
if uncached_ips:
    result = ip2geo.convert_ips(ips=uncached_ips)
    for entry in result['data']:
        cached_results[entry['ip']] = entry
        redis.setex(f'geo:{entry["ip"]}', 3600, json.dumps(entry))

# Apply to dataframe
log_df['country'] = log_df['ip'].map(
    lambda ip: cached_results.get(ip, {}).get('continent', {}).get('country', {}).get('code')
)

For a 10-million-row log file with 100,000 unique IPs, this completes in seconds with 100% cache utilization, vs hours of serial API calls.

When NOT to Cache (Or Cache Differently)

A few scenarios where the default caching pattern needs adjustment:

Real-time fraud detection

If you’re scoring fraud risk in real time, you want the most recent VPN/proxy classification. A 5-minute cache is usually still fine (commercial VPNs don’t add new IPs every minute), but if you’re seeing fraud patterns that suggest “fresh” IPs are slipping through, drop the TTL.

Compliance audit trail

If you’re logging geo data for an audit trail, cache for performance but always store the result alongside the original request so the audit log captures what was used at decision time, not what the cache happens to return on a later read.

Multi-tenant SaaS

Each tenant might have different freshness requirements. Either offer a cache TTL setting per-tenant, or keep a short global TTL and let downstream consumers re-cache as they need.

Monitoring Cache Performance

Three metrics to track:

Hit rate

% of geo lookups served from cache. Target: >90% in steady state.

Cache size

Memory used by the cache. Track to catch unbounded growth.

API call rate

Direct calls to the upstream API. Should grow much slower than your request rate.

If your hit rate is low, increase the TTL or the cache size. If your cache size is exploding, check for cache-key bugs (canonicalization issues, query parameters bleeding into keys, etc.).

TL;DR

  • Cache always. It’s the single biggest cost-and-latency win in IP geolocation.
  • Default TTL: 5 minutes. Tune up for cost-sensitive services, down for fraud-sensitive ones.
  • Multi-instance services need Redis (or equivalent). In-process alone has poor cross-instance hit rates.
  • Layer in-process + Redis for best results: 95%+ hits at sub-ms, fall back to ~5ms Redis hits.
  • Cache the full response. Memory is cheap; flexibility matters.
  • Cache failures too. Negative caching with a short TTL prevents retry storms.
  • Use cache-aware bulk lookups for log enrichment and batch jobs.
  • Monitor hit rate, size, and upstream call rate. If hit rate is low, something’s wrong.

For Rate-limiting topics, see Rate Limiting an IP Lookup API. For language-specific implementations, see Node.js, Python, and PHP guides — all of which include working cache examples.

Get Started

Convert IPs into accurate location data in milliseconds.

Sign up today and get 1,000 free monthly stored conversions, and discover why developers trust us for fast, reliable, and affordable IP conversions.