Graphql N+1 Triage: FetchProjectContainerRepositories
Summary
FetchProjectContainerRepositories ranked among the top N+1 candidates on gitlab.com, based on 24 hours of production log data.
It's estimated to cost 20.5 avoidable queries per minute.
See https://gitlab.com/groups/gitlab-org/-/epics/23614 for how this list was built and the method to follow.
Numbers
| Metric | Value |
|---|---|
| Requests in 24h | 2,961 |
| N+1 risk | 0.95 |
| p95 queries on main replica | 422 |
| p95 DB duration (s) | 0.28 |
| Estimated savings per minute | 20.5 |
Where the query comes from
No .graphql file in gitlab-org/gitlab defines this operation, so it comes from an external client (IDE extension, CLI, or a third-party API user). Reconstruct the selection from json.graphql.used_fields in Kibana.
Primary type read, inferred from the operation name: ContainerRepository. Owning group: group::container registry.
Reconstructed selection
Rebuilt from json.graphql.used_fields of one request sampled on 2026-09-24 (3,169 requests that day). The log lists fields per type, not per position, so a field used on a type shows up under every place that type appears, and arguments and fragments are lost.
query FetchProjectContainerRepositories {
project {
containerRepositories {
nodes {
id
name
path
location
createdAt
}
pageInfo {
hasNextPage
endCursor
}
}
}
}What to do
- Find the query source: the
.graphqlfile in the repo, orjson.graphql.used_fieldsin Kibana for external clients. - Write a throwaway request spec, run it against a small and a larger page, and record queries with
ActiveRecord::QueryRecorder.new(skip_cached: false). - Fix per-row growth with lookahead preloads,
BatchLoader::GraphQL, or a policy preloader inbefore_connection_authorization. - Add a permanent N+1 spec that asserts query counts don't grow with page size.
- After deploy, confirm the savings estimate dropped in the Lens table.
Acceptance criteria
- Per-row queries identified and listed in this issue.
- Fix merged.
- Permanent N+1 spec added.
- Savings in the Lens table dropped after deploy, or this issue is closed with an explanation of why the spread is fixed cost.