NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Each HTTP provider — google, ipinfo, ip2location and maxmind_web — sends through http-client-rate-limits-for-laravel, so calls are paced before they hit a third-party API instead of burning through 429s. Every provider has its own budget, keyed geolocation:{provider}:{owner}. The offline maxmind_database and default providers make no network calls and are never throttled.

Default budgets

Providerlimit / perEnv
google50 / secondGEOLOCATION_GOOGLE_RATELIMIT, …_PER
ipinfo60 / minuteGEOLOCATION_IPINFO_RATELIMIT, …_PER
ip2location60 / minuteGEOLOCATION_IP2LOCATION_RATELIMIT, …_PER
maxmind_web60 / minuteGEOLOCATION_MAXMIND_WEB_RATELIMIT, …_PER

All budgets are adaptive and pace by default.

Per-provider keys

Each HTTP provider carries a rate_limits block under services.<provider>:

'ipinfo' => [
    // ...
    'rate_limits' => [
        'enabled' => env('GEOLOCATION_IPINFO_RATELIMIT_ENABLED', true),
        'owner' => env('GEOLOCATION_RATELIMIT_OWNER', 'app'),
        'limit' => env('GEOLOCATION_IPINFO_RATELIMIT', 60),
        'per' => env('GEOLOCATION_IPINFO_RATELIMIT_PER', 'minute'), // second|minute|hour|day
        'adaptive' => env('GEOLOCATION_IPINFO_RATELIMIT_ADAPTIVE', true),
        'max_wait' => env('GEOLOCATION_IPINFO_RATELIMIT_MAX_WAIT'), // ms; null = pace, set = fail fast
        'jitter' => env('GEOLOCATION_IPINFO_RATELIMIT_JITTER'), // ms; null = none
    ],
],
KeyDefaultEnv (google shown)Purpose
enabledtrueGEOLOCATION_GOOGLE_RATELIMIT_ENABLEDfalse sends with a plain, unthrottled client.
ownerappGEOLOCATION_RATELIMIT_OWNEROwner segment of the budget key (shared by all providers). A blank value is not set (the default); a non-string value throws.
limit50 (google) / 60GEOLOCATION_GOOGLE_RATELIMITMax requests per window, at least 1.
persecond (google) / minuteGEOLOCATION_GOOGLE_RATELIMIT_PERWindow: second, minute, hour or day; a blank value is not set (the default); anything else throws InvalidConfigurationException.
adaptivetrueGEOLOCATION_GOOGLE_RATELIMIT_ADAPTIVEHonour the provider’s 429 Retry-After.
max_waitnullGEOLOCATION_GOOGLE_RATELIMIT_MAX_WAITFail-fast ceiling in ms (0 or more); null or blank = pace (wait).
jitternullGEOLOCATION_GOOGLE_RATELIMIT_JITTERRandom spread in ms added to deferrals (0 or more); null or blank adds none.
GEOLOCATION_RATELIMIT_OWNER=app
GEOLOCATION_GOOGLE_RATELIMIT=50
GEOLOCATION_GOOGLE_RATELIMIT_PER=second
GEOLOCATION_IPINFO_RATELIMIT=60
GEOLOCATION_IPINFO_RATELIMIT_PER=minute
GEOLOCATION_IPINFO_RATELIMIT_MAX_WAIT=2000   # fail fast instead of waiting more than 2 s

Pace or fail fast

By default the limiter waits until the window frees, then sends. Set max_wait (milliseconds) to fail fast instead: when a deferral would exceed it, RateLimitExceededException is thrown. It extends GeolocationException, carries ->provider, and implements the toolkit’s HasRetryAfter contract:

use RoundlyConsulting\Geolocation\Exceptions\RateLimitExceededException;

try {
    $location = Geolocation::locateIp($request->ip());
} catch (RateLimitExceededException $e) {
    $e->provider;   // e.g. 'ipinfo'

    return response('Location lookup is busy, please retry.', 429)
        ->header('Retry-After', (string) $e->retryAfterSeconds());
}

With adaptive on, a provider’s 429 Retry-After tunes the limiter for the next call, while the failed response itself still degrades to null so the pipeline moves on.

Bulk work

batch() and distanceMatrix() run through the same limiter, so bulk lookups pace themselves under the provider’s budget automatically.

Shared budgets across workers

The rate-limit package uses an in-memory store by default — one budget per process, fine for a single worker or CLI run. For an account quota shared across workers and servers, point it at a shared store in its own config (HTTP_CLIENT_RATE_LIMITS_STORE); this package does not force one:

// config/http-client-rate-limits.php — one budget across every worker and server
'store' => \RoundlyConsulting\HttpClientRateLimits\Store\RedisStore::class,

Laravel’s native ->retry(times, delay) still applies for transient connection errors — the rate limiter is an extra, outgoing pacing layer, not a replacement.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.