Add an application rate limit to merge request rebase

What does this MR do and why?

You can rebase a merge request in three ways: the REST API (PUT /projects/:id/merge_requests/:merge_request_iid/rebase), the Rebase button, and the /rebase quick action. All three schedule the same rebase through MergeRequest#rebase_async, and none of them has an application rate limit.

This MR adds a merge_requests_rebase limit of 30 rebases per minute for each user, shared by the three entry points. It's behind the merge_requests_rebase_rate_limit feature flag, which is off by default. Admins can change the limit with the merge_requests_rebase_limit application setting (stored in the rate_limits jsonb column, so no migration), or set it to 0 to turn it off.

Each entry point checks the limit after its own permission checks, so requests that fail them don't count. Over the limit:

Entry point Response
REST API 429 Too Many Requests with Retry-After, like other API rate limits
Rebase button 429 with merge_error. The widget shows "Failed to rebase: Too many rebases have been requested. Try again later."
/rebase quick action Nothing is scheduled, and the quick action shows "Too many rebases have been requested. Try again later.", like its other rebase failures

Evidence for the default limit from production logs: !267972 (comment 4168788537)

Rollback: disable merge_requests_rebase_rate_limit, or set merge_requests_rebase_limit to 0.

Before enabling on GitLab.com

The remaining rate limit process steps (stakeholders, release post, Criticality 2 change request, and the GitLab.com rate limits docs) are tracked in the rollout issue, #638595. The flag is off by default, so merging doesn't change any limit.

References

Detailed context for reviewers and AI agents

Shape

  • merge_requests_rebase_limit in the rate_limits jsonb application setting, with JSON schema entry, numericality validation through the existing list, the helper's visible attributes, and a settings API parameter. The setting follows the thread resolution limit (!264595 (merged)).
  • Labkit rule merge_requests_rebase (limit_merge_request_rebases_by_user), characteristic user, period 1 minute.
  • Gitlab::MergeRequestRebaseRateLimit holds the key, scope, and flag check, so the three entry points stay consistent. It follows Gitlab::NotesCreateRateLimit (!265720 (merged)).
    • The controller and the API call check_merge_request_rebase_rate_limit! (check_rate_limit!), so throttled requests get the standard response and an auth log entry. As with other application rate limits, requests that send the rate limit bypass header aren't limited.
    • The quick action calls .throttled? and reports the limit through @execution_message[:rebase], like its other rebase failures.

Choices to confirm

  • Key per user, not per merge request. The limit counts all of a user's rebases, whichever merge request they're for.
  • Default 30 every minute per user. Agreed with product. Last week it would have blocked 257 rebase requests (0.09%) from 6 users, all mainly using automated clients. The busiest minute for one user was 60 requests. See the evidence comment.
  • Flag: gitlab_com_derisk, user actor, so we can roll out to ourselves first and then by percentage of users.
  • No Admin UI. Like the other flagged 19.6 limits, the setting is only available through the settings API and Rails console while it's in beta.

Docs

doc/api/settings.md (history entry and attribute row), doc/rate_limits/_index.md, and doc/api/openapi/openapi_v3.yaml.

How to set up and validate locally

  1. In the Rails console, enable the flag and lower the limit:

    Feature.enable(:merge_requests_rebase_rate_limit)
    ApplicationSetting.current.update!(merge_requests_rebase_limit: 2)
  2. Pick a merge request whose source branch you can push to.

  3. Rebase it three times within a minute, waiting for each rebase to finish:

    • API: curl --request PUT --header "PRIVATE-TOKEN: <token>" "http://gdk.test:3000/api/v4/projects/<project id>/merge_requests/<iid>/rebase". The third request returns 429.
    • Rebase button: the third click shows "Failed to rebase: Too many rebases have been requested. Try again later."
    • Quick action: comment /rebase. The third comment shows "Too many rebases have been requested. Try again later."
  4. Disable the flag (Feature.disable(:merge_requests_rebase_rate_limit)) and repeat. No rebase is limited.

  5. Re-enable the flag, set the limit to 0, and repeat. No rebase is limited.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Eugenia Grieff

Merge request reports

Loading
Loading