Commit a677bb36 authored by Nidhey Indurkar's avatar Nidhey Indurkar 💻
Browse files

fix(rate_limit): raise regex match timeout to 50ms

The 5ms budget fired 9,511 times in the 24h after deploy and ~624/hour
since, all fail-open, all on git HTTP paths.

The matches themselves are not slow: against the 6935 real paths that
timed out, the median is 0.087ms and the worst 1.484ms, with none above
2ms. Ruby's regex timeout counts wall-clock time, so a match billed for a
GC pause exceeds the budget without doing regex work, and GC pauses on
GitLab.com's git fleet have a p50 of ~72ms.

50ms is 34x the slowest real path and still bounds catastrophic
backtracking. Under 4x CPU oversubscription it fires on 0.16% of the
corpus against 5ms's 1.33%, so it reduces the fail-opens roughly 8x
rather than eliminating them.

Also replaces the benchmark note above the constant, which cited a
measurement that never included repository_git_lfs_route_regex.

Refs gitlab-com/gl-infra/production-engineering#28882

Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent f9b508ea
Loading
Loading
Loading
Loading
Loading
+7 −5
Original line number Diff line number Diff line
@@ -39,11 +39,13 @@ module Labkit
      # unbounded elsewhere), and a timeout reaches Evaluator's fail-open
      # rescue, so the request goes unlimited.
      #
      # 5ms is ~119x the slowest real match measured against GitLab's
      # RackAttack path patterns on inputs up to 16KB (worst: 0.0419ms, none
      # timed out). Re-measure if a rule ever needs nested quantifiers or
      # backreferences, which are what make backtracking exponential.
      MATCH_TIMEOUT_SECONDS = 0.005
      # 50ms is a safety net against catastrophic backtracking, not a
      # performance bound: real matches are far cheaper (median 0.087ms, worst
      # 1.484ms across 6935 paths that timed out in production at 5ms). Those
      # timeouts were wall-clock stalls, since Ruby counts real time and GC
      # pauses on GitLab's git fleet have a p50 of ~72ms. Under 4x CPU
      # oversubscription 50ms still fires on 0.16% of those matches, 5ms 1.33%.
      MATCH_TIMEOUT_SECONDS = 0.05

      def self.build(input)
        case input