Traffic and blocks
After each response, the middleware records what each rule did with the request, in the counter store (ADR-0013): per rule and minute, its hits (the requests it checked), its blocks (the requests an enforcing rule answered 429) and its would_blocks (the requests a dry-run rule would have). The scheduler then moves the numbers into the dynamic_throttle_metrics_hourly table, one row per rule and hour. Publish the package's newest migrations and run php artisan migrate to create it.
- Your scheduler must run. The store keeps the numbers for 2 hours only.
php artisan dynamic-throttle:flush-metricsruns every minute, and adds each minute once, even when it runs again or on two servers at once.php artisan dynamic-throttle:prune --hours=720runs daily, and deletes the rows of hours that ended longer ago thantelemetry.retention_hours(720, 30 days, and at most 1,000,000).telemetry.schedulefalse registers neither. - Who it blocks. On Redis and Valkey, it also keeps per rule and hour the 100 clients it blocked or would have blocked most, as identity hashes, and how many different clients those were, in each row's
clients. That is what tells you whether a dry-run rule would block a few heavy clients or many ordinary ones. Below a sample rate of 1,clientscounts each sampled client once, so it is a lower bound. The cache driver keeps neither, and leavesclientsnull. - The live feed. On Redis and Valkey, it also keeps the last hour's blocks and would-blocks, for the dashboard's Activity page and
GET api/feed: per minute, the newest 500, each with its time, rule, method, the route's template such asorders/{order}, the client's IP, whole, and the user the request already held, asApp\Models\User:42. It never keeps the request's path or query string, a token, a header's value or a custom scope's value, and it never looks a user up. A method longer than 16 letters, which a client can send as_method, shows asOTHER. Withdynamic.throttleas global middleware, a 429 comes before routing, so a blocked request's entry has no route; a would-block's has one. Each minute's list expires 61 minutes after it is created, and the entries go in the request's one telemetry call, sampled with it (ADR-0024's amendment).telemetry.live_feed(DYNAMIC_THROTTLE_LIVE_FEED) false turns it off, if your app must not keep raw IPs, even for an hour. The cache driver keeps no feed. An entry adds about 50 µs to a blocked request's write after the response, and nothing to an allowed request (the benchmark). - Its cost. On the response path, about 5 µs. The write comes after the response is sent: on Redis, a request's numbers are one script call, or one call per key on Redis Cluster. On another cache store, they are an
add()and anincrement()per rule and metric, which on Memcached costs a worker about 1 ms per request (the benchmark). On a busy app,telemetry.sample_rate(DYNAMIC_THROTTLE_SAMPLE_RATE, from 0 to 1, default 1) records only that share of requests, each weighed so the totals stay right on average.telemetry.enabled(DYNAMIC_THROTTLE_TELEMETRY) turns recording off. - Failures. A write that fails is reported, at most once per cooldown, and never affects the request. While the counter store's cooldown runs, nothing is written, so during an outage the numbers under-count.
- The hours are in UTC, whatever your app's time zone. A rule's rows stay after the rule is deleted, until they are pruned.
The dashboard and the JSON API will read these numbers. Until then, the HourlyMetric model reads them.