Make rate limit configurable for Dependency Scanning SBOM scan API
Release notes
Problem to solve
As determined in the workload analysis and capacity planning, even with caching in place we might still have outlier projects with higher needs.
Inncreasing the default limit is not a sustainable approach and would risk ovewhelming the instance infrastructure if used by too many projects. Instead, we should keep a baseline that addresses most usages but allows outliers to configure particular limits if they need it (and are allowed to).
Proposal
Make the existing API rate limits for Sbom Scan uploads and downloads configurable.
- dependency_scanning_sbom_scan_api_upload
- dependency_scanning_sbom_scan_api_download
Telemetry Data Context available in https://gitlab.com/gitlab-org/architecture/readiness/-/merge_requests/35#note_2898032185 (internal)
Rate Limit Configuration Options - GitLab Duo Summary
| Option | Self-Managed | GitLab.com | Effort | Maintenance | Projects Helped | Complexity | Abuse Risk |
|---|---|---|---|---|---|---|---|
| 1. Rack::Attack | Low | Low | All | Low | Low | ||
|
2. Instance + Allowlist |
Low | Minimal | All + 2-5 | Low | Low | ||
| 3a. Namespace Bounded | Medium | Medium | All | Medium | Medium | ||
| 3b. Project Allowlist | Medium | Medium | All | Medium | Low | ||
| 3c. Project Quota | High | High | All | High | Low | ||
| 4. Project Settings | High | High | All | High | High | ||
| 5. Plan Limits | Low | Low | None | Low | N/A |
Recommended Solution: Hybrid Approach - Option 2 (Instance-level) + Manual Allowlist**
For self-managed:
-
✅ Admins configure instance-wide rate limits -
✅ Solves 100% of self-managed cases
For GitLab.com:
-
✅ Keep default at 400/hour (serves 99%+ of projects) -
✅ Manual allowlist for the 2-5 extreme outliers- GitLab team manually adds project IDs to allowlist
- Allowlisted projects get higher limit (e.g., 3000/hour)
- Requires support ticket + justification
- Reviewed quarterly
Reasoning:
- Telemetry shows: P99 usage is 40-55 SBOMs/hour (well within current 400/hour limit)
- Outliers are minimal: Only 2-5 projects exceed limits even with 80% cache hit rate
- Self-managed solved: Instance admins can configure limits based on their infrastructure capacity
- GitLab.com solved: Manual allowlist for the handful of extreme outliers (requires support ticket)
-
Low complexity: Uses existing
ApplicationRateLimiterframework, no new UI/API needed for allowlist - Pragmatic: Solves 100% of self-managed + 99.99%+ of GitLab.com with minimal engineering effort
Implementation:
- Add rate limit settings to
application_settings.rate_limitsJSONB column - Update existing rate limit definitions to reference settings via Proc
- Add Admin UI controls for self-managed configuration
- For GitLab.com: Manual project allowlist maintained by GitLab team (internal configuration)
- Monitor telemetry; if outliers grow significantly (>20 projects), reconsider namespace-level configuration
When to reconsider:
- Outliers grow to 20+ projects
- Pattern emerges (e.g., all large enterprises need this)
- Self-service becomes important for business reasons
- Support ticket volume becomes unsustainable
Alternative solution - Option 2 (Instance-level) and Investigate the Outliers
Before implementing complex solutions to support outliers on gitlab.com, understand why these projects need 500-2,700 SBOMs/hour:
Questions to ask:
- Are they using the feature correctly?
- Is this legitimate usage?
- Can we optimize their workflow?
Detailed Analysis of All Options
Rate Limit Configuration Options - Detailed Review
Option 1: Rack::Attack (Instance-level only)
Description: HTTP middleware-level rate limiting
Scope: Instance-level (per user or per IP)
Configuration: application_settings.rate_limits JSONB column
Platform Support:
-
Self-managed:
✅ Admin configurable via UI -
GitLab.com:
❌ Only GitLab team can configure
Implementation:
- Extend
Gitlab::RackAttackandGitlab::RackAttack::Request - Add settings to Admin area form (
app/views/admin/application_settings/_ip_limits.html.haml) - Update JSON schema validator
- Document in User and IP rate limits docs and Application Settings API
Verdict:
- Wrong layer (HTTP middleware, not application logic)
- No namespace/project-level customization possible
- SBOM API already uses
ApplicationRateLimiter, not Rack::Attack
Option 2: ApplicationRateLimiter with Instance Settings ⭐ RECOMMENDED
Description: Instance-level configuration using existing ApplicationRateLimiter framework
Scope: Instance-level only
Configuration: application_settings fields referenced via Proc in rate_limits hash
Platform Support:
-
Self-managed:
✅ Solves the problem - Admins can configure per-instance limits via Admin UI/API -
GitLab.com:
⚠️ Only GitLab team can adjust base limits, but can use manual allowlist for outliers
Current Implementation:
# ee/lib/ee/gitlab/application_rate_limiter.rb
dependency_scanning_sbom_scan_api_upload: { threshold: 400, interval: 1.hour }
dependency_scanning_sbom_scan_api_download: { threshold: 6000, interval: 1.hour }
Proposed Implementation:
# ee/lib/ee/gitlab/application_rate_limiter.rb
dependency_scanning_sbom_scan_api_upload: {
threshold: -> {
if project.id.in?(application_settings.dependency_scanning_sbom_scan_api_project_allowlist || [])
application_settings.dependency_scanning_sbom_scan_api_upload_limit_allowlist
else
application_settings.dependency_scanning_sbom_scan_api_upload_limit
end
},
interval: 1.hour
}
# application_settings (new fields in rate_limits JSONB)
dependency_scanning_sbom_scan_api_upload_limit: 400 # Default
dependency_scanning_sbom_scan_api_upload_limit_allowlist: 3000 # For outliers
dependency_scanning_sbom_scan_api_project_allowlist: [67034029, 59473315] # Manual list
dependency_scanning_sbom_scan_api_download_limit: 6000
Implementation Steps:
- Add new fields to
application_settings.rate_limitsJSONB column - Update JSON schema validator for
rate_limitscolumn - Update rate limit definitions to use Procs referencing settings
- Add Admin UI controls in
app/views/admin/application_settings/(for self-managed) - Document in Application Settings API docs
- For GitLab.com: GitLab team manually updates allowlist via Rails console/internal tooling
GitLab.com Allowlist Process:
- User hits rate limit repeatedly
- Opens support ticket with justification
- GitLab team reviews usage patterns
- If legitimate, adds project ID to allowlist
- Quarterly review of allowlist to remove inactive projects
Effort: Low (1-2 days)
Maintenance: Minimal (quarterly allowlist review)
Verdict:
- Simple implementation using proven framework
- Solves 100% of self-managed cases
- Solves GitLab.com outliers (2-5 projects) with minimal overhead
- No complex UI/API needed for allowlist
- Can iterate to namespace-level config if outliers grow
Option 3a: ApplicationRateLimiter with Bounded Namespace Settings
Description: Namespace-level configuration with hard upper bounds to prevent abuse
Scope: Namespace-level with instance fallback and ceiling enforcement
Configuration: namespace_settings table with validation constraints
Platform Support:
-
Self-managed:
✅ Admins set instance defaults and max ceiling, namespace owners can override within bounds -
GitLab.com:
✅ Namespace admins can increase limits within GitLab-defined ceiling
Implementation:
Database Schema:
# namespace_settings table (new columns)
dependency_scanning_sbom_scan_api_upload_limit: integer # Nullable, overrides instance default
dependency_scanning_sbom_scan_api_download_limit: integer
Application Settings:
# application_settings
dependency_scanning_sbom_scan_api_upload_limit: 400 # Default
max_dependency_scanning_sbom_scan_api_upload_limit: 2000 # Ceiling
dependency_scanning_sbom_scan_api_download_limit: 6000
max_dependency_scanning_sbom_scan_api_download_limit: 10000
Validations:
# app/models/namespace_settings.rb
validates :dependency_scanning_sbom_scan_api_upload_limit,
numericality: {
less_than_or_equal_to: -> { Gitlab::CurrentSettings.max_dependency_scanning_sbom_scan_api_upload_limit },
greater_than: 0,
allow_nil: true
}
Runtime Logic:
def threshold
namespace_limit = namespace_settings&.dependency_scanning_sbom_scan_api_upload_limit
instance_default = application_settings.dependency_scanning_sbom_scan_api_upload_limit
instance_max = application_settings.max_dependency_scanning_sbom_scan_api_upload_limit
[namespace_limit || instance_default, instance_max].min
end
Implementation Steps:
- Add columns to
namespace_settingstable (migration) - Add validations to
NamespaceSettingsmodel - Add ceiling settings to
application_settings - Create service to read namespace settings and pass runtime threshold
- Expose via Groups API with proper authorization
- Add Admin UI for namespace admins (optional)
- Add audit logging for limit changes
- Document in user and admin docs
Safety Guarantees:
- Hard upper bound per namespace (e.g., max 2000/hour)
- Instance admin controls the ceiling
- Model-level validation prevents exceeding ceiling
- Audit trail for all changes
Risk on GitLab.com:
-
⚠️ Every namespace could set limits to ceiling -
⚠️ Potential for widespread infrastructure impact if many namespaces increase limits -
⚠️ Difficult to predict total load
Effort: Medium (2-3 weeks)
Maintenance: Medium (UI, API, validation, monitoring)
Verdict:
- Solves for 2-5 projects with weeks of engineering effort
- Introduces abuse risk on GitLab.com (unbounded namespace adoption)
- Better suited if outliers grow to 20+ projects across many namespaces
Option 3b: ApplicationRateLimiter with Project-level Allowlist
Description: Namespace admins can allowlist specific projects for higher limits
Scope: Project-level exceptions to instance limit, managed at namespace level
Configuration: namespace_settings.dependency_scanning_sbom_scan_api_project_allowlist (array of project IDs)
Platform Support:
-
Self-managed:
✅ Admins can allowlist specific projects -
GitLab.com:
✅ Namespace admins can allowlist up to X projects (e.g., max 10)
Implementation:
Database Schema:
# namespace_settings table (new column)
dependency_scanning_sbom_scan_api_project_allowlist: integer[], default: []
Application Settings:
# application_settings
dependency_scanning_sbom_scan_api_upload_limit: 400 # Default
dependency_scanning_sbom_scan_api_upload_limit_allowlist: 2000 # For allowlisted projects
dependency_scanning_sbom_scan_api_project_allowlist_limit: 10 # Max projects per namespace
Validations:
# app/models/namespace_settings.rb
validates :dependency_scanning_sbom_scan_api_project_allowlist,
length: {
maximum: -> { Gitlab::CurrentSettings.dependency_scanning_sbom_scan_api_project_allowlist_limit }
}
validate :allowlisted_projects_belong_to_namespace
Runtime Logic:
def threshold
if project.id.in?(namespace_settings&.dependency_scanning_sbom_scan_api_project_allowlist || [])
application_settings.dependency_scanning_sbom_scan_api_upload_limit_allowlist
else
application_settings.dependency_scanning_sbom_scan_api_upload_limit
end
end
Safety Guarantees:
- Limited number of allowlisted projects per namespace (e.g., 10)
- Two-tier limits: standard (400/hour) vs allowlist (2000/hour)
- Bounded resource impact:
10 projects × (2000 - 400) = 16,000 additional requests/hour per namespace - Easier to audit and monitor than namespace-wide limits
Effort: Medium (2-3 weeks)
Maintenance: Medium (API, validation, monitoring)
Verdict:
- Limits blast radius compared to 3a
- Still requires significant engineering for 2-5 projects
- Better suited if outliers are distributed across many namespaces
Option 3c: Per-Project Limits with Namespace Quota
Description: Namespace admins configure custom limits per project, with quota on number of projects
Scope: Project-level limits configured at namespace level, with quota constraints
Configuration: namespace_settings stores per-project limit mappings with quota enforcement
Platform Support:
-
Self-managed:
✅ Admins control quota and ceiling, namespace owners configure projects -
GitLab.com:
✅ Namespace admins can configure limits for up to X projects
Implementation:
Database Schema:
# namespace_settings table (new column)
dependency_scanning_sbom_scan_api_project_limits: jsonb # { "project_id": limit }
# Example: { "278964": 1500, "123456": 2000 }
Application Settings:
# application_settings
dependency_scanning_sbom_scan_api_upload_limit: 400 # Default
max_dependency_scanning_sbom_scan_api_upload_limit: 2000 # Ceiling per project
dependency_scanning_sbom_scan_api_project_limit_quota: 10 # Max projects per namespace
Validations:
# app/models/namespace_settings.rb
validates :dependency_scanning_sbom_scan_api_project_limits,
length: {
maximum: -> { Gitlab::CurrentSettings.dependency_scanning_sbom_scan_api_project_limit_quota }
}
validate :project_limits_within_ceiling
def project_limits_within_ceiling
max_allowed = Gitlab::CurrentSettings.max_dependency_scanning_sbom_scan_api_upload_limit
dependency_scanning_sbom_scan_api_project_limits.each do |project_id, limit|
if limit > max_allowed
errors.add(:dependency_scanning_sbom_scan_api_project_limits,
"cannot exceed maximum of #{max_allowed}")
end
end
end
Runtime Logic:
def threshold
custom_limit = namespace_settings
&.dependency_scanning_sbom_scan_api_project_limits
&.dig(project.id.to_s)
custom_limit || application_settings.dependency_scanning_sbom_scan_api_upload_limit
end
Safety Guarantees:
- Fixed upper bound per project (e.g., 2000/hour)
- Linear scaling by namespace: Max impact =
quota × ceilingper namespace - Controlled blast radius: Only X projects per namespace affected
- Instance admin controls quota, ceiling, and default
- Audit trail for all changes
Advantages over 3a and 3b:
- More flexible than 3b (custom limits per project, not just binary allowlist)
- Safer than 3a (quota limits number of projects affected)
- Granular control for namespaces with variable needs
Effort: Medium-High (3-4 weeks)
Maintenance: Medium-High (UI, API, validation, monitoring, audit)
Verdict:
- Highest engineering cost for 2-5 projects
- Best suited if outliers grow to 50+ projects with varying needs
- Premature optimization for current problem size
Option 4: ApplicationRateLimiter with Project Settings
Description: Project-level configuration with namespace/instance fallback
Scope: Project-level with cascading from namespace and instance
Configuration: project_settings table + runtime override
Platform Support:
-
Self-managed:
✅ Admins set defaults, project owners can override -
GitLab.com:
⚠️ Every project could increase limits (high abuse risk)
Implementation: Similar to Option 3a but using project_settings table
Verdict:
- Higher abuse potential (more projects than namespaces)
- More granular than needed (SBOM scans already scoped to project)
- Harder to audit and control
Option 5: Plan Limits
Description: Different limits per subscription tier (Free/Premium/Ultimate)
Scope: Per-tier (Free/Premium/Ultimate)
Configuration: plan_limits table
Platform Support:
-
Self-managed:
✅ Admins can set per-plan limits -
GitLab.com:
✅ Different limits per tier
Implementation:
# plan_limits table
dependency_scanning_sbom_scan_api_upload_limit: integer
dependency_scanning_sbom_scan_api_download_limit: integer
Verdict:
- Feature is Ultimate-only, no tier differentiation possible
- Can't solve outlier problem (all outliers are already on Ultimate)
- Would only work if feature was available across tiers
Implementation plan
Retained solution: Instance-level configuration using existing ApplicationRateLimiter framework without manual allow-list. See #581567 (comment 2913633227)
Implementation Steps:
- Add new fields to
application_settings.rate_limitsby following guidance in https://docs.gitlab.com/development/application_settings/#add-a-new-application-setting - Update
ApplicationRateLimiterto use Procs referencing settings: https://gitlab.com/gitlab-org/gitlab/-/blob/28aa596af556aa811b8b77df5fb370228db030b6/ee/lib/ee/gitlab/application_rate_limiter.rb#L41-42 - Update Admin settings UI and add a form to update these limits udner the Security and Compliance section
- Document in Application Settings API docs
- Document in Application Limits
- Document in the main Dependency Scanning documentation and any other required place (migration guide?)
-
❌ Do not document in Rate Limits. While we effectively use the rate limit framework, this is not a public facing API and from a user perspective this is rather a limit on the DS feature.