Investigate production label counts to size a labels-per-work-item limit
Summary
The Work Item REST API returns all of a work item's labels as full objects, inline and unpaginated, via ?features=labels — on both the list and show endpoints. There is currently no limit on how many labels a work item can have (assignees are capped at 200; labels are not). On the list endpoint this multiplies (page size × labels/item), creating an unbounded response — a potential DoS vector.
This is a prerequisite investigation: before we choose a limit, we need production data on how many labels work items actually have, so the cap sits safely above real-world usage and doesn't break existing workflows.
Background
- Measured impact: a single work item with 150 labels returns ~13.5 KB via
?features=labels; a 100-item list page ≈ 1.35 MB, unbounded. - Pre-existing gap: the legacy Issues REST API has the same behavior via
with_labels_details=true. GraphQL is the only safe surface (labels are a paginated connection, max 100/page). - Precedent: assignees are already capped —
Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS = 200.
Goal of this issue
Run the queries below on a prod replica / Database Lab (not the primary — label_links is large) and record the results, so we can pick a limit above the p99.9 with headroom.
Distribution query
SELECT
count(*) AS work_items_with_labels,
max(cnt) AS max_labels,
round(avg(cnt), 2) AS avg_labels,
percentile_cont(0.50) WITHIN GROUP (ORDER BY cnt) AS p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY cnt) AS p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY cnt) AS p99,
percentile_cont(0.999) WITHIN GROUP (ORDER BY cnt) AS p999,
count(*) FILTER (WHERE cnt > 50) AS over_50,
count(*) FILTER (WHERE cnt > 100) AS over_100,
count(*) FILTER (WHERE cnt > 200) AS over_200,
count(*) FILTER (WHERE cnt > 500) AS over_500
FROM (
SELECT target_id, count(*) AS cnt
FROM label_links
WHERE target_type = 'Issue' -- work items are stored in the issues table
GROUP BY target_id
) per_item;Worst-offenders query (is the tail legit usage or abuse?)
SELECT target_id, count(*) AS labels
FROM label_links
WHERE target_type = 'Issue'
GROUP BY target_id
ORDER BY labels DESC
LIMIT 25;Results (to fill in)
| metric | value |
|---|---|
| work_items_with_labels | |
| max / avg | |
| p50 / p95 / p99 / p99.9 | |
| over_50 / over_100 / over_200 / over_500 |
Decision this feeds
- Pick a configurable limit above the p99.9 (with headroom).
- Do not hardcode — expose it as a configurable application/plan limit so we don't break self-managed on upgrade.
- Validate on delta (enforce in
Issuable::Callbacks::Labels: reject only whennew > limit && new > existing), so existing over-limit items are grandfathered and you only get an error when adding beyond the cap. - Defense-in-depth: bound the new REST API response (summary/count on list + full on show).
Related
- Design doc:
handbook.gitlab.com/handbook/engineering/architecture/design-documents/work_item_rest_api/ - Part of epic #21503 (closed)
- Related: single-item auth check #603898
- A security/DoS issue for this is expected to be raised by the team — should be cross-linked.