The Steam Web API allows 100 000 requests per API key per day. The connector
enforces this via saloonphp/rate-limit-plugin
and throws SteamRateLimitException once the budget is spent — locally, before a
request leaves your process. The exception carries the Limit that tripped:
use Fkrzski\SteamApiSdk\Exceptions\SteamRateLimitException;
try { $connector->send($request)->dto();} catch (SteamRateLimitException $e) { $seconds = $e->limit->getRemainingSeconds();}The budget resets at 00:00 UTC whatever date.timezone says, so getRemainingSeconds()
counts down to the next midnight UTC. Valve documents no reset time; UTC is an assumption.
Every answer Steam sends counts — a 4xx, 5xx or 429 as much as a 200 — through
send(), sendAsync() and pool() alike. A request that got no answer counts nothing,
though Steam may still have billed one that timed out. Steam bills the budget to the key, so
by default a connector without one counts nothing locally, and neither does a
request sent without the key.
After a 429
Section titled “After a 429”When Steam itself answers 429, the connector raises the same exception and refuses every
request locally until Steam’s Retry-After passes. Without the header it holds them back for
tooManyRequestsCooldown seconds, 60 unless you set it:
$connector = new SteamConnector(new SteamConfig( apiKey: $_ENV['STEAM_API_KEY'], tooManyRequestsCooldown: 300,));A Retry-After from Steam still wins. The cool-down applies with a key or without and
whatever rateLimits holds, [] included, and it lives in the store next to the budget, per
key, with requests sent without the key locked apart. A value under 1 throws
InvalidRateLimitException as the config is built.
Your own limits
Section titled “Your own limits”rateLimits swaps the daily budget for your own Limits — a key with a bigger budget, a
share of one that several apps use, a window per second:
use Saloon\RateLimitPlugin\Limit;
$connector = new SteamConnector(new SteamConfig( apiKey: $_ENV['STEAM_API_KEY'], rateLimits: [ Limit::allow(500_000) ->everySeconds(86_400, 'utc_midnight') ->setExpiryTimestamp(new DateTimeImmutable('tomorrow', new DateTimeZone('UTC'))->getTimestamp()), Limit::allow(10)->everySeconds(1)->sleep(), ],));The list replaces the default whole, so a per-second limit alone leaves the day unmetered.
The first line keeps a daily budget that resets at 00:00 UTC, like the default. [] meters
nothing locally, and a 429 from Steam still throws either way.
The limits apply without an API key too, and still count per key, so connectors sharing a
store don’t drain each other; requests sent without the key
count them apart. ->sleep() waits for the window to pass
instead of throwing, blocking the process meanwhile, in pool() as well. Two limits with one
name throw InvalidRateLimitException as the config is built; give one of them a ->name().
Checking before you send
Section titled “Checking before you send”The plugin’s own methods read the store without sending anything, for a job that would rather wait than fail:
if ($connector->hasReachedRateLimit(0.9)) { $seconds = $connector->getExceededLimit(0.9)->getRemainingSeconds();}The argument is the share of a limit already spent, so 0.9 answers once 90 000 of the
100 000 are gone; without it a limit has to be spent whole. Both see the lock after a 429 too.
They read the key’s counters; for requests sent without it, ask a connector built without a key
on the same store.
$connector->useRateLimitPlugin(false) turns the limiter off altogether: nothing is counted
or refused locally, and a 429 surfaces as SteamApiException with code 429 rather than
SteamRateLimitException. To stop counting but keep the lock, pass rateLimits: [] instead.
Multiple API keys
Section titled “Multiple API keys”The budget is tracked per API key, so one process — or one shared store — can serve several keys without them draining each other’s quota.
Requests sent without the key
Section titled “Requests sent without the key”Steam serves some methods anonymously, stats()->currentPlayers() among them, so the connector
sends them without the key, like any request marked
SendsNoApiKey. Steam bills them to no key,
so the connector limits them as a connector built without one would: they never touch the daily
budget, go through while the key is spent or locked, and a 429 on one of them locks only
requests without the key, under SteamConnector:anonymous.
rateLimits count them apart too, so a window per second lets that many through with the key
and as many again without it.
Sharing the budget across processes
Section titled “Sharing the budget across processes”The default MemoryStore keeps its backing array static, so the budget is process-wide.
That is fine for a script, but every FPM or queue worker then gets a counter of its own
and the real quota can be overrun several times over. In tests, a new SteamFake
clears it.
Inject a shared store instead — any Saloon\RateLimitPlugin\Contracts\RateLimitStore
is accepted:
$connector = new SteamConnector(new SteamConfig( apiKey: $_ENV['STEAM_API_KEY'], rateLimitStore: $store,));use Saloon\RateLimitPlugin\Stores\PredisStore;
$store = new PredisStore(new Predis\Client());use Saloon\RateLimitPlugin\Stores\RedisStore;
$redis = new Redis();$redis->connect('127.0.0.1');
$store = new RedisStore($redis);use Saloon\RateLimitPlugin\Stores\PsrStore;
// $cache is any Psr\SimpleCache\CacheInterface implementation.$store = new PsrStore($cache);use Saloon\RateLimitPlugin\Stores\FileStore;
$store = new FileStore(__DIR__.'/storage/steam-rate-limit');