[Bug]: GitLab merge request reads spend the shared GitLab.com rate limit: project lookup on every detail read, full MR read on every checks poll#24190

opened6d agobySsungkhum

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Related, but not the same problem: #21888 (Azure DevOps: one-minute sweep with no batching), #23702 (GitHub: idle branch discovery), #3581 (GitHub gh pr list polling), #13496 (client re-fetching detail on focus, closed). This report is the GitLab read path: each refresh costs more GitLab requests than it needs, and on GitLab.com those requests come out of a budget the user's own browser also uses.

Area

apps/server (packages/source-control-gitlab, apps/server/src/pullRequest)

Steps to reproduce

  1. Use a GitLab.com account on the Free plan, with glab authenticated as that user.
  2. Link one or two open merge requests to threads.
  3. Keep a client open on a view that shows one of those merge requests, and keep using the app (the live refresh stops after 6 idle minutes).
  4. Sample glab child processes of the T3 Code server, or read server.trace.ndjson. Run any glab api call with GLAB_DEBUG_HTTP=true to confirm that one glab api spawn is one HTTP request.

Expected behavior

A merge request that is not changing should be read about once per refresh window, with one request per read. Detail, checks and the sync sweep should share that read.

Actual behavior

From the local server trace over 26 minutes (desktop server, one remote client), the server spawned glab 86 times:

spawnscaller chain
34PullRequestSyncReactor.sweep → syncGroup → PullRequestService.persistedRead → PullRequestReadCache.get
25ws.rpc.pullRequests.detail → PullRequestService.withRoutingCredential
16ws.rpc.pullRequests.checks → PullRequestService.withRoutingCredential (once every ~60 s)
6ws.rpc.server.discoverSourceControl
3ThreadTitleRegenerationService → GitLabSourceControlProvider.readLinkSubject

A process sample over 12.5 minutes on the same machine put T3 Code at 39 of about 70 glab calls from every process using that GitLab account. During active use, one open merge request that had not changed in 9 hours was read every minute.

Two GitLab-specific costs drive this (references are on main @ 9f660d38):

  1. Each detail read is two requests. getChangeRequest runs getMergeRequestDetail and getProjectMergeCapabilities together (packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts:145-152). The second is GET projects/<path>?license=false (GitLabPullRequestCli.ts:1221-1224). Project merge settings almost never change, but they are re-read with every merge request detail.
  2. Each checks refresh is a full merge request read that no other cache shares. On GitLab, getChangeRequestChecks calls the same getMergeRequestDetail (GitLabPullRequestProvider.ts:139-143, GET …/merge_requests/:iid?include_diverged_commits_count=true, GitLabPullRequestCli.ts:854-863). PullRequestService caches checks for 15 s (CHECKS_CACHE_TTL, PullRequestService.ts:137, cache at :2779-2805), separately from the 60 s detail cache (:133). Clients refresh checks every 45–60 s (apps/web/src/hooks/usePullRequestChecksRefresh.ts:14-18), so nearly every checks poll goes to GitLab, even when a detail read for the same merge request is seconds old.

GitLabCli recognizes a rate limit only after a command has failed with it (GitLabCli.ts:142). Reads use glab api without --include, so the server never sees RateLimit-Remaining and cannot slow down before GitLab returns 429.

Impact

Minor bug or occasional failure.

GitLab.com reported RateLimit-Name: burst_traffic_per_user_free_plan, RateLimit-Limit: 100 on these reads. GitLab documents the Free-plan per-user limits (100/minute, 5,000/hour) as covering API, web and Git-over-HTTPS requests together (https://docs.gitlab.com/user/gitlab_com/rate_limits/). When T3 Code's reads, an agent creating merge requests, and normal browsing land in the same minute, the user gets "Rate limit exceeded" on gitlab.com in their browser, not just failed reads in T3 Code. Polling peaks during active use, which is also when the user is most likely to be on the GitLab website.

Proposed fix

  1. Cache project merge capabilities per project (for example per host + repository + credential, for 15–30 minutes, dropped when a merge fails because of a setting). This halves every GitLab detail read.
  2. Let checks share the merge request read on GitLab. Because GitLab checks and detail come from one endpoint, either serve checks from a detail read cached in the last 60 s, or give the GitLab provider a short in-flight plus TTL memo for getMergeRequestDetail keyed by repository + number, so detail, checks and the sync sweep share one read. At minimum, CHECKS_CACHE_TTL should not be shorter than the client cadence when a provider's checks cost as much as its detail.
  3. (Can be a follow-up) Read GitLab's RateLimit-Remaining/RateLimit-Reset with glab api --include and pass them to SourceControlRateLimit, so background reads (sync sweep, checks polling) wait when the per-user budget runs low instead of only pausing after a 429. On GitLab.com, a 429 also blocks the user's browser.

Items 1 and 2 are small and contained to the GitLab provider and the checks cache. Together they would roughly halve the client-driven reads above (25 + 16 spawns).

Version or commit

T3 Code (Beta) 0.1.1, desktop build installed 2026-10-21 (it includes #22073). Code references checked against main @ 9f660d38.

Environment

macOS (Darwin 25.3), desktop app as the server, one remote client through the relay. GitLab.com Free plan, glab 1.101.0.

Logs or stack traces

The counts above come from server.trace.ndjson: processRunner.runProcessCore spans with process.command = glab, grouped by their parent chain. Single-request reads were confirmed with GLAB_DEBUG_HTTP=true.

Workaround

Settle threads (or unlink their merge requests) once they no longer need tracking, and avoid leaving a merge request view open on another device.

Investigated with a coding agent on the affected machine.