Is my endpoint being rate limited?
For a team that owns an endpoint or a feature and wants to know whether rate limiting is rejecting its traffic. Everything here reads the Rate limiting detail dashboard and needs no PromQL.
For a single client or a single request, the dashboard is the wrong tool. Go to LabKit cohort flags for the log lines instead.
Start from what you have
Section titled “Start from what you have”| You have | Where to look |
|---|---|
| A request path | The Request path variable, then 429s by request path |
A feature, or an ApplicationRateLimiter key | The Limiter variable, then Selected limiter: checks by action |
| A 429 response in hand | Its RateLimit-Name header names the throttle, then the log lines |
Both variables list only series that have actually blocked. A limiter or path that has never rejected anything does not appear, which is itself an answer.
By request path
Section titled “By request path”429s by request path counts 429s from Workhorse rather than from the limiter, so it counts every rejection whatever produced it, including Cloudflare and HAProxy.
route is Workhorse’s own route table, which is much coarser than an endpoint.
^/api/ is the catch-all for every API path with no Workhorse route of its own, and it carries roughly half of all 429s:
sum(rate(gitlab_workhorse_http_requests_total{env="gprd", code="429", route="^/api/"}[1h]))/sum(rate(gitlab_workhorse_http_requests_total{env="gprd", code="429"}[1h]))So a REST endpoint is usually invisible inside that one series. Use the panel to find the path worth looking at, not to prove a particular endpoint is being limited.
Web requests carry no route label at all and arrive as a single unlabelled series, shown as web (no route label).
Selected path: 429 share of its own traffic divides the same recording rule by itself without the code filter.
The share is what a caller experiences; the raw count tells you how big the path is.
By feature
Section titled “By feature”Every ApplicationRateLimiter key becomes its own limiter named applimiter_<key>, so picking your feature’s key answers the question for that feature directly.
Selected limiter: checks by action gives the allow and block rate.
It reads checks_total, which increments once per check call, so it is a request rate and the block fraction is meaningful.
Do not build that fraction from rule_evaluations_total: that one increments once per rule evaluated, so a request matching several rules is counted several times.
Rule names inside a limiter are written per limit rather than derived from the key, so they will not match the limiter name.
The one exception is <key>_bypass, a synthetic rule that terminates evaluation for bypass-header traffic without touching Redis.
Which rule blocked
Section titled “Which rule blocked”Selected limiter: blocks by rule breaks the selected limiter down by the rule that decided block.
The limiter panel says a caller was rejected; this one says what rejected them.
The same per-rule counting applies, so a request matching several rules appears in several series.
What this cannot tell you
Section titled “What this cannot tell you”The LabKit metrics carry no path. There is no way to go from a limiter or a rule to a request path in Grafana, in either direction.
A blocked request never reaches the application, so it is assigned no feature_category and no endpoint_id.
The metrics middleware wraps the limiter, so those requests are still counted in the Rails SLIs, but under endpoint_id="unknown" and feature_category="unknown".
A per-feature view built on feature_category therefore cannot see them.
For which client was rejected, and how far over the limit they were, read the Rate_Limit_Block line.
LabKit cohort flags has its fields and the sampling rule that decides when it is written.