The Steam Web API allows 100 000 requests per API key per day. The connector enforces this
through saloonphp/rate-limit-plugin and
throws SteamRateLimitException once the budget is spent, before a request leaves your process.
The budget resets at 00:00 UTC, whatever your timezone says. Valve documents no reset time, so
UTC is an assumption.
Every answer Steam sends counts, a 4xx, 5xx or 429 as much as a 200, and so does every
request in a pool. The counter is keyed by
the API key, hashed so the key never lands in the store, and two keys draw down two budgets.
With no key configured nothing is counted, since Steam bills the budget to the key.
The cache store
Section titled “The cache store”The counter lives in a Laravel cache store, not per-process memory, so with a shared driver (Redis, database, Memcached) every FPM worker, queue worker and Octane process draws down one budget. It is the default store until you name another:
STEAM_API_RATE_LIMIT_STORE=redisGive it a store of its own: cache:clear flushes the default one, resetting the count while
Steam’s own carries on. A store that evicts keys under memory pressure, such as Redis with an
eviction policy or Memcached, can drop the counter too. A name cache.stores does not define
raises InvalidSteamRateLimitOptionException on your first Steam call, rendered as a 500.
Your own limits
Section titled “Your own limits”limits replaces the daily budget with limits of your own: a key with a bigger budget, a share
of one that several apps use, a window per second.
'rate_limit' => [ 'store' => env('STEAM_API_RATE_LIMIT_STORE'),
'limits' => [ ['allow' => 500_000, 'every' => 'day'], ['allow' => 10, 'every' => 1, 'sleep' => true], ],],Each entry allows allow requests every every seconds, or every 'day', a window that closes
at 00:00 UTC. A 'day' entry allowing 100 000 keeps counting where the default left off, while
changing an entry’s allow or every restarts its count.
steam-api.rate_limit.limits |
What is counted locally |
|---|---|
null — the default |
100 000 a day, with an API key only |
[] |
nothing |
| a list of entries | those limits, with a key or without |
The list replaces the default whole, so a per-second limit alone leaves the day unmetered.
Entries are plain arrays rather than the plugin’s Limit objects because config:cache cannot
write those out.
A value the connector cannot take raises InvalidSteamRateLimitOptionException naming its key,
such as steam-api.rate_limit.limits.1.every, on your first Steam call, rendered as a 500. Two
entries with the same allow and every would share one counter, so they are rejected too.
Waiting instead of failing
Section titled “Waiting instead of failing”'sleep' => true waits for the window to pass instead of throwing, blocking the process
meanwhile, in a pool as well. On a 'day' entry it waits until 00:00 UTC, so keep sleep to
short windows.
After a 429
Section titled “After a 429”When Steam itself answers 429, the connector raises SteamRateLimitException and refuses
every request locally until Steam’s Retry-After passes. Without the header it holds them back
for the cool-down, 60 seconds unless you set it:
STEAM_API_RATE_LIMIT_COOLDOWN=300The cool-down applies with a key or without and whatever limits holds, [] included, and it
lives in the store next to the budget, per key. A value that is not a whole number of seconds,
at least 1, raises InvalidSteamRateLimitOptionException.
Queued jobs
Section titled “Queued jobs”Left alone, a job that hits a spent limit fails like on any other exception.
Queue\Middleware\RespectsSteamRateLimit releases it for what is left of the window instead,
before it runs once a limit is spent, or when a request inside it raises
SteamRateLimitException:
use Fkrzski\LaravelSteamApiSdk\Queue\Middleware\RespectsSteamRateLimit;
public function middleware(): array{ return [new RespectsSteamRateLimit];}
public function retryUntil(): DateTime{ return now()->addDay();}A release counts as an attempt, so a job waiting out a daily window wants retryUntil()
rather than a small $tries. The middleware releases through InteractsWithQueue, and lets
a job without it run unguarded. With nothing metered locally only a 429 from Steam holds a job
back.
To check without sending, Steam::connector()->hasReachedRateLimit(0.9) reads the store the
way the middleware does — see
Checking before you send.
Checking the budget
Section titled “Checking the budget”php artisan about reports three rows for the rate limit — the
full listing is in Configuration:
- Rate Limit Store — the cache store the counter lives in, marked
(default)whilesteam-api.rate_limit.storeis unset. - Requests Remaining — what is left of each local limit, read from that store, such as
99,412 of 100,000 per day, 9 of 10 per 1s (sleeps).not meteredforlimitsset to[],not metered (no API key)for the default with no key configured, andUNKNOWNwhile another row readsINVALID, since the connector cannot be built. - 429 Cool-down — the seconds a
429withoutRetry-Afterholds requests back,60s (default)until you set it.
Each of the three reads INVALID for a value of its own the connector does not take.