Skip to content

[TT-17049] Replace unbounded regex caches with a bounded LRU cache to prevent memory leaks#8239

Merged
MFCaballero merged 13 commits into
masterfrom
TT-17049
Jun 3, 2026
Merged

[TT-17049] Replace unbounded regex caches with a bounded LRU cache to prevent memory leaks#8239
MFCaballero merged 13 commits into
masterfrom
TT-17049

Conversation

@MFCaballero

@MFCaballero MFCaballero commented May 20, 2026

Copy link
Copy Markdown
Contributor

Description

TT-17049 — benchmark results

Hot-path benchmarks for github.com/TykTechnologies/tyk/regexp and
github.com/TykTechnologies/tyk/internal/httputil, measured to validate
that bounding the regex caches with expirable.LRU did not regress
request-handling cost.

  • Run date: 2026-05-21
  • Host: Apple M1 Pro · 10 cores · 16 GB RAM · macOS 15.6.1
  • Toolchain: host go1.25.1 darwin/arm64; benchmarks executed via
    Tyk's local go wrapper inside a Docker container (go1.25.9 linux/arm64),
    which is why the bench output reports goos: linux, goarch: arm64.
  • Iterations: -count=10 -benchtime=1s — gives real 95% CIs on every
    row (the previous -count=5 runs printed ± ∞ ¹).

What changed

Two independent wins compose:

  1. Bounded LRU swap (internal/cache.Cacheexpirable.LRU). On its
    own this adds a small overhead from the LRU's recency-update mutex on
    every Get — visible as the small Replace*/FindAll* regressions
    above. Allocations stay identical.

  2. Removed defensive re.Copy() in cache.getRegexp. The Copy was
    added in 2018-06-06 (PR NormalisePath and RegExp optimizations #1689), eight months before Go 1.12 (Feb 2019)
    made *regexp.Regexp safe for concurrent reads without copying. Once
    removed, every cache hit drops 1 allocation and 160 B of garbage, and
    net wall time on Compile/Match drops well below master.

The Copy was pre-existing on master, not added by TT-17049. The Go
deprecation tooling surfaced the opportunity.

Methodology

# Master worktree:
git worktree add -f /tmp/tyk-master-bench master
cd /tmp/tyk-master-bench
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... > master.bench

# TT-17049 branch (includes new httputil bench):
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... ./internal/httputil/... > branch.bench

benchstat master.bench branch.bench > benchstat.txt

Results — master vs TT-17049

Wall time

Benchmark Master (ns/op) Branch (ns/op) Δ p
RegExpCompile 121.90 86.72 −28.86% 0.000
RegExpCompilePOSIX 123.05 74.70 −39.29% 0.000
RegExpMustCompile 124.90 87.91 −29.62% 0.000
RegExpMustCompilePOSIX 124.05 75.48 −39.15% 0.000
RegExpMatchString 193.90 164.50 −15.14% 0.000
RegExpMatch 195.00 166.30 −14.74% 0.000
RegExpString 0.5086 0.5130 +0.87% (sub-ns) 0.000
RegexpReplaceAllString 74.79 77.46 +3.58% 0.002
RegexpReplaceAllLiteralString 74.71 78.41 +4.95% 0.000
RegexpReplaceAllStringFunc 106.80 112.20 +5.06% 0.000
RegexpFindAllString 82.02 85.20 +3.89% 0.000
RegexpFindAllStringSubmatch 82.20 85.71 +4.26% 0.000
RegexpFindStringSubmatch 77.05 76.79 ~ 0.853
Geomean (comparable subset) −12.89%

~ = no statistically significant change (p > 0.05).

Memory

Benchmark Master (B/op) Branch (B/op) Δ p
RegExpCompile 176 16 −90.91% 0.000
RegExpCompilePOSIX 176 16 −90.91% 0.000
RegExpMustCompile 176 16 −90.91% 0.000
RegExpMustCompilePOSIX 176 16 −90.91% 0.000
RegExpMatchString 176 16 −90.91% 0.000
RegExpMatch 176 16 −90.91% 0.000
(others: ReplaceAll/Find) 0 0
Geomean delta −66.94%

Allocations

Benchmark Master Branch Δ p
Compile / Match paths 2 1 −50% 0.000
Replace / Find paths 0 0
Geomean delta −27.38%

New benchmarks (branch-only)

These cover paths added by TT-17049 — no master comparator since the LRU
API didn't exist there.

Benchmark ns/op B/op allocs/op
Cache_HitParallel 164.4 0 0
Cache_MissAndAdd_Saturated 670.4 160 4
Regexp_MatchString_Hit 156.7 16 1
Regexp_Compile_Hit 73.94 16 1
Regexp_Compile_Miss 4136 5815 69
PreparePathRegexp_Hit 164.0 47 2

What these tell us

  • Cache hit is ~56× faster than a cold compile (74 ns vs 4136 ns).
    Customer pattern sizes are ~70 KB vs the trivial ^abc.*$ benchmarked
    here, so production hit-vs-miss savings are even larger.

  • HitParallel at 164 ns / 0 allocs at GOMAXPROCS=10: the
    expirable.LRU mutex is not pathological under concurrent reads.
    The plan's "P1 sharded-LRU contingency" remains unwarranted.

  • Saturated MissAndAdd at 670 ns/op: if a workload's distinct-pattern
    working set exceeds RegexpCacheMaxEntries, every miss costs
    (670 + 4136) ≈ 4.8 µs (eviction + cold compile). That's the
    5-minute eviction-summary warning's reason to exist.

  • PreparePathRegexp_Hit 2 allocs: the fmt.Sprintf cache key,
    not the LRU itself. A future micro-opt (pooled strings.Builder)
    could remove these; not worth doing now.

Why the Copy() removal works

cache.getRegexp previously returned v.(*regexp.Regexp).Copy() for
defensive concurrency. As of Go 1.12 (Feb 2019), the stdlib documents
*regexp.Regexp as safe for concurrent reads via its Match*, Find*,
ReplaceAll*, etc. methods without copying. Only Longest() mutates —
and Tyk's Regexp.Longest() wrapper is dead code (a logic-flipped
nil-guard that would only invoke the underlying Longest() on a nil
receiver, panicking).

The race tests in regexp/cache_test.go::TestCache_ConcurrentReads_SharedRegex
hammer 14 read methods × 200 goroutines × 200 iterations on a shared
cached regex and pass cleanly under go test -race.

Related Issue

Motivation and Context

How This Has Been Tested

Screenshots (if appropriate)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring or add test (improvements in base code or adds test coverage to functionality)

Checklist

  • I ensured that the documentation is up to date
  • I explained why this PR updates go.mod in detail with reasoning why it's required
  • I would like a code coverage CI quality gate exception and have explained why

Ticket Details

TT-17049
Status In Test
Summary Memory fragmentation and unbounded RSS growth in CoProcess bridge

Generated at: 2026-05-28 12:22:16

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🎯 Recommended Merge Targets

Based on JIRA ticket TT-17049: Memory fragmentation and unbounded RSS growth in CoProcess bridge

Fix Version: Tyk 5.8.15

Required:

  • release-5.8 - Minor version branch for 5.8.x patches - required for creating Tyk 5.8.15
  • master - Main development branch - ensures fix is in all future releases

Fix Version: Tyk 5.13.1

Required:

  • release-5.13 - Minor version branch for 5.13.x patches - required for creating Tyk 5.13.1
  • master - Main development branch - ensures fix is in all future releases

Fix Version: Tyk 5.14.0

⚠️ Warning: Expected release branches not found in repository

Required:

  • master - No matching release branches found. Fix will be included in future releases.

📋 Workflow

  1. Merge this PR to master first

  2. Cherry-pick to release branches by commenting on the merged PR:

    • /release to release-5.8
    • /release to release-5.13
  3. Automated backport - The bot will automatically create backport PRs to the specified release branches

@probelabs

probelabs Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces a comprehensive set of improvements focused on memory stability, performance, and runtime efficiency. The core change replaces unbounded regular expression caches with a bounded LRU (Least Recently Used) cache, preventing potential memory leaks caused by a large number of unique regex patterns. This is complemented by a significant performance optimization that removes a legacy defensive regexp.Copy() call, drastically reducing memory allocations and CPU time on cache hits.

Additionally, the PR integrates automaxprocs to automatically tune GOMAXPROCS for optimal CPU utilization in containerized environments and resolves a race condition in the BaseMiddleware logger to enhance overall system reliability.

Files Changed Analysis

  • Total Changes: The PR modifies 30 files with 1331 additions and 134 deletions, indicating a substantial refactoring of the caching layer and related components.
  • Key Patterns:
    • Caching Overhaul: The primary change involves replacing a custom unbounded cache with hashicorp/golang-lru/v2/expirable across the regexp/ package and internal/httputil/mux.go. A new internal/cache/ package is introduced to centralize LRU utilities, including an EvictionLogger for monitoring.
    • New Configuration: New gateway settings (regexp_cache_max_entries, disable_regexp_cache_bound, disable_auto_max_procs) are added in config/config.go and its JSON schema, providing operators with control over the new caching and runtime behaviors.
    • Runtime Optimization: A new dependency, go.uber.org/automaxprocs, is added and implemented in gateway/runtime.go to automatically align GOMAXPROCS with container CPU quotas.
    • Concurrency Fix: A mutex is added to gateway/middleware.go to make BaseMiddleware.Copy() thread-safe, addressing a race condition. This is validated by new tests in gateway/coprocess_test.go and gateway/middleware_test.go.
    • Extensive Testing: The PR adds a significant number of new unit tests, race condition tests, and benchmarks to validate correctness, stability, and performance improvements.

Architecture & Impact Assessment

  • What this PR accomplishes:

    1. Memory Stability: Mitigates the risk of out-of-memory errors by enforcing a configurable size limit on all major regex caches.
    2. Performance Improvement: Reduces CPU time and memory allocations for regex-heavy operations, as demonstrated by the comprehensive benchmarks in the PR description.
    3. Enhanced CPU Utilization: Improves gateway throughput in containerized environments by automatically setting GOMAXPROCS based on available CPU resources.
    4. Increased Reliability: Resolves a race condition in the core middleware component, preventing potential panics under concurrent load.
  • Key technical changes introduced:

    • Replaced a custom, unbounded in-memory cache with a bounded expirable.LRU cache.
    • Integrated go.uber.org/automaxprocs to manage GOMAXPROCS at gateway startup.
    • Removed the defensive regexp.Regexp.Copy() call, leveraging the fact that *regexp.Regexp has been safe for concurrent reads since Go 1.12.
    • Added mutex protection to BaseMiddleware.Copy() to ensure thread-safe logger duplication.
  • Affected system components:

    • Gateway Caching: All systems relying on the regexp package for pattern compilation and matching now use the new bounded LRU cache.
    • Gateway Routing: The path-to-regexp cache used by the router in internal/httputil/mux.go is now also bounded.
    • Gateway Configuration & Startup: New options in tyk.conf are processed during the gateway's initialization sequence to configure the new behaviors.
    • Middleware Engine: The BaseMiddleware component is now hardened against concurrency issues, affecting the entire middleware stack.

System Impact Diagram

graph TD
    subgraph "Configuration (tyk.conf)"
        A[regexp_cache_max_entries]
        B[disable_regexp_cache_bound]
        C[disable_auto_max_procs]
    end

    subgraph "Gateway Startup"
        D["gateway.afterConfSetup()"]
    end

    subgraph "Affected Components"
        E["Regex Caches (regexp, httputil)"]
        F["Go Runtime (GOMAXPROCS)"]
        G["Middleware Engine (BaseMiddleware)"]
    end

    A --|Configures LRU bounds|--> D
    B --|Opts-out of bounds|--> D
    C --|Controls tuning|--> D

    D --|Initializes Bounded LRU|--> E
    D --|Tunes via automaxprocs|--> F
    G --|Race condition fixed in Copy|--> G
Loading

Scope Discovery & Context Expansion

  • The refactoring of the regexp package is a system-wide change that enhances the stability and performance of any feature relying on pattern matching, including URL rewriting, header matching, and request validation. This is a critical reliability improvement for the entire gateway.
  • The integration of automaxprocs is a process-level optimization that will benefit all Tyk deployments in modern containerized environments (like Kubernetes), improving resource efficiency without requiring manual configuration.
  • The fix in BaseMiddleware.Copy() hardens the entire middleware pipeline. While the bug was likely identified in the CoProcess middleware (as hinted by the JIRA ticket and new tests), the underlying race condition could have affected any middleware, making this a crucial stability enhancement for the whole system.
Metadata
  • Review Effort: 4 / 5
  • Primary Label: enhancement

Powered by Visor from Probelabs

Last updated: 2026-05-28T12:25:36.581Z | Triggered by: pr_updated | Commit: ce523cd

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs

probelabs Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

✅ Security Check Passed

No security issues found – changes LGTM.

✅ Architecture Check Passed

No architecture issues found – changes LGTM.

✅ Performance Check Passed

No performance issues found – changes LGTM.

Quality Issues (1)

Severity Location Issue
🟠 Error gateway/middleware.go:323
The `Copy` method on `BaseMiddleware` uses a `loggerMu` mutex to protect access to the `logger` field, but this mutex is not defined within the `BaseMiddleware` struct itself. This suggests an incomplete refactoring, as the lock will not be part of the copied object, potentially leading to incorrect locking behavior if different instances of `BaseMiddleware` share a non-existent mutex.
💡 SuggestionAdd a `loggerMu sync.Mutex` field to the `BaseMiddleware` struct to ensure that each instance has its own mutex, and that the lock is correctly associated with the data it protects. This will make the concurrent access protection explicit and correct.
🔧 Suggested Fix
type BaseMiddleware struct {
	Spec     *APISpec
	Proxy    ReturningHttpHandler
	Gw       *Gateway
	logger   *logrus.Entry
	loggerMu sync.Mutex
}

Powered by Visor from Probelabs

Last updated: 2026-05-28T12:24:17.235Z | Triggered by: pr_updated | Commit: ce523cd

💡 TIP: You can chat with Visor using /visor ask <your question>

@MFCaballero MFCaballero added the deps-reviewed Dependency changes reviewed and approved for CI execution label May 20, 2026
@MFCaballero MFCaballero changed the title init [TT-17049] Replace unbounded regex caches with a bounded LRU cache to prevent memory leaks May 22, 2026

@vladzabolotnyi vladzabolotnyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insane piece of work! Well done 🚀

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Jira Linter Failed

Commit: ce523cd
Failed at: 2026-05-28 12:22:18 UTC

The Jira linter failed to validate your PR. Please check the error details below:

🔍 Click to view error details
failed to validate Jira issue: jira ticket TT-17049 has status 'In Test' but must be one of: In Dev, In Code Review, Ready For Dev, Dod Check

Next Steps

  • Ensure your branch name contains a valid Jira ticket ID (e.g., ABC-123)
  • Verify your PR title matches the branch's Jira ticket ID
  • Check that the Jira ticket exists and is accessible

This comment will be automatically deleted once the linter passes.

@bojank93

bojank93 commented May 28, 2026

Copy link
Copy Markdown

Quinn — Tyk QA Review | PR #8239 · Jira TT-17049 · 2026-05-28


PR Summary

This PR fixes two separate production regressions reported by customer RBFCU on 8 Gateway nodes running 5.9.2: (1) unbounded RSS growth caused by regexp.compileCache and httputil.pathRegexpCache accumulating compiled-regex entries with no capacity cap, and (2) a nil-pointer panic in BaseMiddleware.Copy() triggered when CustomMiddlewareResponseHook.Init fails to wire its logger. The fix replaces both caches with expirable.LRU behind atomic.Pointer for lock-free hot-path access, adds operator-tunable config (regexp_cache_max_entries, disable_regexp_cache_bound), patches the nil-logger dereference, and wires automaxprocs so GC pacing reflects cgroup CPU quotas. The implementation matches all four acceptance criteria.


Jira Alignment

Field Value
Ticket TT-17049 — Memory fragmentation and unbounded RSS growth in CoProcess bridge
Type Bug
Status In Test
Acceptance Criteria Met? ⚠️ Partial — AC2 hot-reload path lacks test coverage

Criteria gaps:

# Acceptance Criterion Covered in PR? Note
1 regexp.compileCache and pathRegexpCache must have a structural upper bound ✅ Yes expirable.LRU with maxEntries enforced; verified by TestCache_EvictsAtCap and TestPathRegexpCache_EvictsAtCap
2 Operator can tune bound and TTL via tyk.conf without restarting; default 5000 ⚠️ Partial Config fields added, atomic swap supports runtime reconfiguration — but no test drives Configure() twice (simulating hot-reload) to confirm the second call takes effect
3 BaseMiddleware.Copy() must not panic; CustomMiddlewareResponseHook.Init must wire logger ✅ Yes Nil-guard with loggerMu in Copy(); Init constructs baseMid and calls SetName before embedding
4 GOMAXPROCS reflects cgroup CPU quota at startup ✅ Yes configureAutoMaxProcs via go.uber.org/automaxprocs; DisableAutoMaxProcs escape hatch provided

Unit & Integration Test Coverage

Package Coverage Tests Added? Gap Description Severity
regexp 71.3% Yes — cache_test.go (+372 lines), regexp_test.go (rewritten) ✅ None
internal/cache 92.2% Yes — eviction_test.go (+105 lines) ✅ None
internal/httputil 86.3% Yes — mux_test.go (+106 lines), mux_bench_test.go ✅ None
gateway Not measured (requires integration build) Yes — coprocess_test.go, middleware_test.go, runtime_test.go, server_regexpcache_test.go configureAutoMaxProcs(disable=false) path not unit tested despite gostub being added as a dependency 🟢 Low
gateway (hot-reload path) N/A No Configure() and ConfigurePathRegexpCache() are only exercised in afterConfSetup; no test calls them a second time to validate that a fresh LRU is correctly installed and the operator-visible configuration change takes effect 🟡 Medium

Notes on test quality:

  • TestCache_ConcurrentReads_SharedRegex: 14 method variants × 200 goroutines × 200 iterations validates the re.Copy() removal claim thoroughly. ✅
  • TestBaseMiddleware_CopyNilLogger_Concurrent: 100 concurrent goroutines confirm the nil-guard is race-clean. ✅
  • server_regexpcache_test.go tests assert warning log text only — not that regexp.Configure() received the correct LRUOptions. The tests are structurally valid but verify the side-effect channel (log) rather than the outcome (cache correctly bounded). Acceptable but note the gap. 🟢 Low

UI Test Coverage (Playwright)

No frontend files changed — section not applicable.


Security Findings

The SentinelOne CNS Scan failure lists 24 issues across node-forge, handlebars, flatted, underscore, fast-uri, and github.com/docker/docker. None of these are Go dependencies; all are JS/toolchain or Docker-layer CVEs. This failure is pre-existing on master and is not caused by this PR. Verify by checking the S1 result on a master branch run — if it also fails there, this PR should not be blocked by it.

No security issues found in the changed Go code.


Code Quality Issues

# Severity Location Issue Recommendation
1 🟢 Low regexp/cache.go maxKeySize = 1024 and maxValueSize = 2048 are declared but unused — these were size-based eviction constants for the old gocache implementation and are dead code in the LRU replacement Remove both constants
2 🟡 Medium regexp/regexp.go, internal/httputil/mux.go expirable.LRU goroutine lifecycle on Configure() calls: when TTL > 0, hashicorp/golang-lru/v2/expirable starts a background cleanup goroutine. When Configure() atomically swaps in a new LRU, the old instance loses its external reference but the goroutine holds a closure reference, preventing GC until the finalizer fires. On a gateway that hot-reloads config frequently this accumulates goroutines. Needs verification: does the version of golang-lru/v2 in use expose a Close() method? If so, call it on the replaced instance before the swap. Check if expirable.LRU has Close()/Stop() and call it on the outgoing instance, or add a code comment acknowledging finalizer-based cleanup is sufficient
3 🟢 Low regexp/regexp.go ResetCache kept as a deprecated shim — confirm it carries a // Deprecated: use Configure instead. godoc comment so consumers know not to add new call sites Add godoc deprecation notice if missing

Tyk Domain Issues

# Severity Location Issue Recommendation
1 🟡 Medium gateway/server.goregexp.Configure() / httputil.ConfigurePathRegexpCache() Hot-reload path untested: AC2 states the operator can tune the bound "without restarting." The atomic-swap design supports this, but there is no test that simulates a config reload by calling Configure() twice with different options and asserting the second config wins. If a future refactor breaks the reload path this will be invisible. Add a table-driven test: call Configure() with opts A, assert cache is bounded to A; call again with opts B, assert cache is now bounded to B
2 🟡 Medium CI — validate check Jira linter is failing. Ticket TT-17049 is in "In Test" status; the linter likely expects the PR to be linked to a ticket in "In Progress" or "In Review." This is a process gate, not a code issue. Transition TT-17049 to the correct status per the team's workflow (typically "In Review" while the PR is open)
3 🟢 Low cli/linter/schema.json New fields (regexp_cache_max_entries, disable_regexp_cache_bound, disable_auto_max_procs) added to schema — confirm they appear in the operator-facing configuration reference / docs site. Raise a docs ticket if not already tracked

What was done well

  • Concurrent safety proof for re.Copy() removal: 14 cache method variants each exercised by 200 goroutines × 200 iterations. This is exactly the right way to justify removing a defensive copy from a hot path.
  • Atomic-pointer cache swap: lock-free access to the LRU in the request hot path while keeping reconfiguration safe. Clean pattern, well-suited to Tyk's high-concurrency workload.
  • Bucketed eviction logger (internal/cache/eviction.go): reusable, idempotent Stop(), ticker-based drain with no goroutine leak on the reporter side. Well-structured and independent of the LRU itself.
  • Minimal nil-guard in Copy(): acquires loggerMu, reads the pointer, nil-checks, returns. No unnecessary lock scope, no behaviour change when logger is present.

Gap Summary

Area Gaps Found Highest Severity
Unit / Integration Tests 2 🟡 Medium
UI Tests (Playwright) 0 — N/A
Security 0 ✅ None
Code Quality 3 🟡 Medium
Tyk Domain 3 🟡 Medium
Jira Alignment 1 criterion gap ⚠️ Partial

Verdict

NEEDS DISCUSSION

Two questions need answers before this can be approved:

  1. expirable.LRU goroutine lifecycle — confirm whether hashicorp/golang-lru/v2/expirable (at the version pinned in go.mod) exposes a Close() or equivalent. If it does, the replaced LRU instance should be closed before the atomic swap to avoid goroutine accumulation on hot-reload. If cleanup is finalizer-driven, add a comment acknowledging this. Either way — a brief comment or a one-line Close() call resolves the concern.

  2. Jira linter failurevalidate CI job fails because the ticket is in "In Test" status. This is a process gate. Move TT-17049 to the expected pre-merge status.

The dead constants (maxKeySize, maxValueSize) and the missing hot-reload test are low-friction fixes that can go in with the above. The SentinelOne and Visor failures appear pre-existing and should not block this PR once confirmed on master.

Core logic is sound, concurrent tests are thorough, and the nil-logger fix is clean. This is close to approval.


Review by Quinn · Tyk QA · 2026-05-28

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
96.8% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@MFCaballero
MFCaballero enabled auto-merge (squash) June 3, 2026 10:44
@MFCaballero
MFCaballero merged commit e6a40c1 into master Jun 3, 2026
69 of 90 checks passed
@MFCaballero
MFCaballero deleted the TT-17049 branch June 3, 2026 11:22
@MFCaballero

Copy link
Copy Markdown
Contributor Author

/release to release-5.13

@MFCaballero

Copy link
Copy Markdown
Contributor Author

/release to release-5.8

@probelabs

probelabs Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

✅ Cherry-pick successful. A PR was created: #8274

@probelabs

probelabs Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ Cherry-pick encountered conflicts. A draft PR was created: #8275

shults pushed a commit that referenced this pull request Jun 3, 2026
… prevent memory leaks (#8239)

<!-- Provide a general summary of your changes in the Title above -->

## Description
# TT-17049 — benchmark results

Hot-path benchmarks for `github.com/TykTechnologies/tyk/regexp` and
`github.com/TykTechnologies/tyk/internal/httputil`, measured to validate
that bounding the regex caches with `expirable.LRU` did not regress
request-handling cost.

- **Run date:** 2026-05-21
- **Host:** Apple M1 Pro · 10 cores · 16 GB RAM · macOS 15.6.1
- **Toolchain:** host `go1.25.1 darwin/arm64`; benchmarks executed via
Tyk's local `go` wrapper inside a Docker container (`go1.25.9
linux/arm64`),
  which is why the bench output reports `goos: linux, goarch: arm64`.
- **Iterations:** `-count=10 -benchtime=1s` — gives real 95% CIs on
every
  row (the previous `-count=5` runs printed `± ∞ ¹`).

## What changed

Two independent wins compose:

1. **Bounded LRU swap** (`internal/cache.Cache` → `expirable.LRU`). On
its
   own this adds a small overhead from the LRU's recency-update mutex on
   every Get — visible as the small `Replace*`/`FindAll*` regressions
   above. Allocations stay identical.

2. **Removed defensive `re.Copy()`** in `cache.getRegexp`. The Copy was
added in 2018-06-06 (PR #1689), eight months before Go 1.12 (Feb 2019)
   made `*regexp.Regexp` safe for concurrent reads without copying. Once
   removed, every cache hit drops 1 allocation and 160 B of garbage, and
   net wall time on Compile/Match drops well below master.

The Copy was pre-existing on master, not added by TT-17049. The Go
deprecation tooling surfaced the opportunity.

## Methodology

```sh
# Master worktree:
git worktree add -f /tmp/tyk-master-bench master
cd /tmp/tyk-master-bench
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... > master.bench

# TT-17049 branch (includes new httputil bench):
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... ./internal/httputil/... > branch.bench

benchstat master.bench branch.bench > benchstat.txt
```

## Results — master vs TT-17049

### Wall time

| Benchmark | Master (ns/op) | Branch (ns/op) | Δ | p |

|------------------------------------|---------------:|---------------:|-------------:|------:|
| `RegExpCompile` | 121.90 | **86.72** | **−28.86%** | 0.000 |
| `RegExpCompilePOSIX` | 123.05 | **74.70** | **−39.29%** | 0.000 |
| `RegExpMustCompile` | 124.90 | **87.91** | **−29.62%** | 0.000 |
| `RegExpMustCompilePOSIX` | 124.05 | **75.48** | **−39.15%** | 0.000 |
| `RegExpMatchString` | 193.90 | **164.50** | **−15.14%** | 0.000 |
| `RegExpMatch` | 195.00 | **166.30** | **−14.74%** | 0.000 |
| `RegExpString` | 0.5086 | 0.5130 | +0.87% (sub-ns) | 0.000 |
| `RegexpReplaceAllString` | 74.79 | 77.46 | +3.58% | 0.002 |
| `RegexpReplaceAllLiteralString` | 74.71 | 78.41 | +4.95% | 0.000 |
| `RegexpReplaceAllStringFunc` | 106.80 | 112.20 | +5.06% | 0.000 |
| `RegexpFindAllString` | 82.02 | 85.20 | +3.89% | 0.000 |
| `RegexpFindAllStringSubmatch` | 82.20 | 85.71 | +4.26% | 0.000 |
| `RegexpFindStringSubmatch` | 77.05 | 76.79 | ~ | 0.853 |
| **Geomean (comparable subset)** | | | **−12.89%** | |

`~` = no statistically significant change (p > 0.05).

### Memory

| Benchmark | Master (B/op) | Branch (B/op) | Δ | p |

|--------------------------|--------------:|--------------:|------------:|------:|
| `RegExpCompile` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpCompilePOSIX` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMustCompile` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMustCompilePOSIX` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMatchString` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMatch` | 176 | **16** | **−90.91%** | 0.000 |
| (others: ReplaceAll/Find)| 0 | 0 | — | — |
| **Geomean delta** | | | **−66.94%** | |

### Allocations

| Benchmark             | Master | Branch | Δ           | p     |
|-----------------------|------:|------:|------------:|------:|
| Compile / Match paths | 2     | **1** | **−50%**    | 0.000 |
| Replace / Find paths  | 0     | 0     | —           | —     |
| **Geomean delta**     |       |       | **−27.38%** |       |

## New benchmarks (branch-only)

These cover paths added by TT-17049 — no master comparator since the LRU
API didn't exist there.

| Benchmark                       | ns/op  | B/op  | allocs/op |
|---------------------------------|-------:|------:|----------:|
| `Cache_HitParallel`             | 164.4  | 0     | 0         |
| `Cache_MissAndAdd_Saturated`    | 670.4  | 160   | 4         |
| `Regexp_MatchString_Hit`        | 156.7  | 16    | 1         |
| `Regexp_Compile_Hit`            | 73.94  | 16    | 1         |
| `Regexp_Compile_Miss`           | 4136   | 5815  | 69        |
| `PreparePathRegexp_Hit`         | 164.0  | 47    | 2         |

### What these tell us

- **Cache hit is ~56× faster than a cold compile** (74 ns vs 4136 ns).
  Customer pattern sizes are ~70 KB vs the trivial `^abc.*$` benchmarked
  here, so production hit-vs-miss savings are even larger.

- **HitParallel at 164 ns / 0 allocs at GOMAXPROCS=10:** the
  `expirable.LRU` mutex is not pathological under concurrent reads.
  The plan's "P1 sharded-LRU contingency" remains unwarranted.

- **Saturated MissAndAdd at 670 ns/op:** if a workload's
distinct-pattern
  working set exceeds `RegexpCacheMaxEntries`, every miss costs
  `(670 + 4136) ≈ 4.8 µs` (eviction + cold compile). That's the
  5-minute eviction-summary warning's reason to exist.

- **`PreparePathRegexp_Hit` 2 allocs:** the `fmt.Sprintf` cache key,
  not the LRU itself. A future micro-opt (pooled `strings.Builder`)
  could remove these; not worth doing now.

## Why the Copy() removal works

`cache.getRegexp` previously returned `v.(*regexp.Regexp).Copy()` for
defensive concurrency. As of Go 1.12 (Feb 2019), the stdlib documents
`*regexp.Regexp` as safe for concurrent reads via its `Match*`, `Find*`,
`ReplaceAll*`, etc. methods without copying. Only `Longest()` mutates —
and Tyk's `Regexp.Longest()` wrapper is dead code (a logic-flipped
nil-guard that would only invoke the underlying `Longest()` on a nil
receiver, panicking).

The race tests in
`regexp/cache_test.go::TestCache_ConcurrentReads_SharedRegex`
hammer 14 read methods × 200 goroutines × 200 iterations on a shared
cached regex and pass cleanly under `go test -race`.

## Related Issue

<!-- This project only accepts pull requests related to open issues. -->
<!-- If suggesting a new feature or change, please discuss it in an
issue first. -->
<!-- If fixing a bug, there should be an issue describing it with steps
to reproduce. -->
<!-- OSS: Please link to the issue here. Tyk: please create/link the
JIRA ticket. -->

## Motivation and Context

<!-- Why is this change required? What problem does it solve? -->

## How This Has Been Tested

<!-- Please describe in detail how you tested your changes -->
<!-- Include details of your testing environment, and the tests -->
<!-- you ran to see how your change affects other areas of the code,
etc. -->
<!-- This information is helpful for reviewers and QA. -->

## Screenshots (if appropriate)

## Types of changes

<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Refactoring or add test (improvements in base code or adds test
coverage to functionality)

## Checklist

<!-- Go over all the following points, and put an `x` in all the boxes
that apply -->
<!-- If there are no documentation updates required, mark the item as
checked. -->
<!-- Raise up any additional concerns not covered by the checklist. -->

- [ ] I ensured that the documentation is up to date
- [ ] I explained why this PR updates go.mod in detail with reasoning
why it's required
- [ ] I would like a code coverage CI quality gate exception and have
explained why




<!---TykTechnologies/jira-linter starts here-->

### Ticket Details

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17049" title="TT-17049"
target="_blank">TT-17049</a>
</summary>

|         |    |
|---------|----|
| Status  | In Test |
| Summary | Memory fragmentation and unbounded RSS growth in CoProcess
bridge |

Generated at: 2026-05-28 12:22:16

</details>

<!---TykTechnologies/jira-linter ends here-->
probelabs Bot pushed a commit that referenced this pull request Jun 3, 2026
… prevent memory leaks (#8239)

<!-- Provide a general summary of your changes in the Title above -->

Hot-path benchmarks for `github.com/TykTechnologies/tyk/regexp` and
`github.com/TykTechnologies/tyk/internal/httputil`, measured to validate
that bounding the regex caches with `expirable.LRU` did not regress
request-handling cost.

- **Run date:** 2026-05-21
- **Host:** Apple M1 Pro · 10 cores · 16 GB RAM · macOS 15.6.1
- **Toolchain:** host `go1.25.1 darwin/arm64`; benchmarks executed via
Tyk's local `go` wrapper inside a Docker container (`go1.25.9
linux/arm64`),
  which is why the bench output reports `goos: linux, goarch: arm64`.
- **Iterations:** `-count=10 -benchtime=1s` — gives real 95% CIs on
every
  row (the previous `-count=5` runs printed `± ∞ ¹`).

Two independent wins compose:

1. **Bounded LRU swap** (`internal/cache.Cache` → `expirable.LRU`). On
its
   own this adds a small overhead from the LRU's recency-update mutex on
   every Get — visible as the small `Replace*`/`FindAll*` regressions
   above. Allocations stay identical.

2. **Removed defensive `re.Copy()`** in `cache.getRegexp`. The Copy was
added in 2018-06-06 (PR #1689), eight months before Go 1.12 (Feb 2019)
   made `*regexp.Regexp` safe for concurrent reads without copying. Once
   removed, every cache hit drops 1 allocation and 160 B of garbage, and
   net wall time on Compile/Match drops well below master.

The Copy was pre-existing on master, not added by TT-17049. The Go
deprecation tooling surfaced the opportunity.

```sh
git worktree add -f /tmp/tyk-master-bench master
cd /tmp/tyk-master-bench
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... > master.bench

go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... ./internal/httputil/... > branch.bench

benchstat master.bench branch.bench > benchstat.txt
```

| Benchmark | Master (ns/op) | Branch (ns/op) | Δ | p |

|------------------------------------|---------------:|---------------:|-------------:|------:|
| `RegExpCompile` | 121.90 | **86.72** | **−28.86%** | 0.000 |
| `RegExpCompilePOSIX` | 123.05 | **74.70** | **−39.29%** | 0.000 |
| `RegExpMustCompile` | 124.90 | **87.91** | **−29.62%** | 0.000 |
| `RegExpMustCompilePOSIX` | 124.05 | **75.48** | **−39.15%** | 0.000 |
| `RegExpMatchString` | 193.90 | **164.50** | **−15.14%** | 0.000 |
| `RegExpMatch` | 195.00 | **166.30** | **−14.74%** | 0.000 |
| `RegExpString` | 0.5086 | 0.5130 | +0.87% (sub-ns) | 0.000 |
| `RegexpReplaceAllString` | 74.79 | 77.46 | +3.58% | 0.002 |
| `RegexpReplaceAllLiteralString` | 74.71 | 78.41 | +4.95% | 0.000 |
| `RegexpReplaceAllStringFunc` | 106.80 | 112.20 | +5.06% | 0.000 |
| `RegexpFindAllString` | 82.02 | 85.20 | +3.89% | 0.000 |
| `RegexpFindAllStringSubmatch` | 82.20 | 85.71 | +4.26% | 0.000 |
| `RegexpFindStringSubmatch` | 77.05 | 76.79 | ~ | 0.853 |
| **Geomean (comparable subset)** | | | **−12.89%** | |

`~` = no statistically significant change (p > 0.05).

| Benchmark | Master (B/op) | Branch (B/op) | Δ | p |

|--------------------------|--------------:|--------------:|------------:|------:|
| `RegExpCompile` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpCompilePOSIX` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMustCompile` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMustCompilePOSIX` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMatchString` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMatch` | 176 | **16** | **−90.91%** | 0.000 |
| (others: ReplaceAll/Find)| 0 | 0 | — | — |
| **Geomean delta** | | | **−66.94%** | |

| Benchmark             | Master | Branch | Δ           | p     |
|-----------------------|------:|------:|------------:|------:|
| Compile / Match paths | 2     | **1** | **−50%**    | 0.000 |
| Replace / Find paths  | 0     | 0     | —           | —     |
| **Geomean delta**     |       |       | **−27.38%** |       |

These cover paths added by TT-17049 — no master comparator since the LRU
API didn't exist there.

| Benchmark                       | ns/op  | B/op  | allocs/op |
|---------------------------------|-------:|------:|----------:|
| `Cache_HitParallel`             | 164.4  | 0     | 0         |
| `Cache_MissAndAdd_Saturated`    | 670.4  | 160   | 4         |
| `Regexp_MatchString_Hit`        | 156.7  | 16    | 1         |
| `Regexp_Compile_Hit`            | 73.94  | 16    | 1         |
| `Regexp_Compile_Miss`           | 4136   | 5815  | 69        |
| `PreparePathRegexp_Hit`         | 164.0  | 47    | 2         |

- **Cache hit is ~56× faster than a cold compile** (74 ns vs 4136 ns).
  Customer pattern sizes are ~70 KB vs the trivial `^abc.*$` benchmarked
  here, so production hit-vs-miss savings are even larger.

- **HitParallel at 164 ns / 0 allocs at GOMAXPROCS=10:** the
  `expirable.LRU` mutex is not pathological under concurrent reads.
  The plan's "P1 sharded-LRU contingency" remains unwarranted.

- **Saturated MissAndAdd at 670 ns/op:** if a workload's
distinct-pattern
  working set exceeds `RegexpCacheMaxEntries`, every miss costs
  `(670 + 4136) ≈ 4.8 µs` (eviction + cold compile). That's the
  5-minute eviction-summary warning's reason to exist.

- **`PreparePathRegexp_Hit` 2 allocs:** the `fmt.Sprintf` cache key,
  not the LRU itself. A future micro-opt (pooled `strings.Builder`)
  could remove these; not worth doing now.

`cache.getRegexp` previously returned `v.(*regexp.Regexp).Copy()` for
defensive concurrency. As of Go 1.12 (Feb 2019), the stdlib documents
`*regexp.Regexp` as safe for concurrent reads via its `Match*`, `Find*`,
`ReplaceAll*`, etc. methods without copying. Only `Longest()` mutates —
and Tyk's `Regexp.Longest()` wrapper is dead code (a logic-flipped
nil-guard that would only invoke the underlying `Longest()` on a nil
receiver, panicking).

The race tests in
`regexp/cache_test.go::TestCache_ConcurrentReads_SharedRegex`
hammer 14 read methods × 200 goroutines × 200 iterations on a shared
cached regex and pass cleanly under `go test -race`.

<!-- This project only accepts pull requests related to open issues. -->
<!-- If suggesting a new feature or change, please discuss it in an
issue first. -->
<!-- If fixing a bug, there should be an issue describing it with steps
to reproduce. -->
<!-- OSS: Please link to the issue here. Tyk: please create/link the
JIRA ticket. -->

<!-- Why is this change required? What problem does it solve? -->

<!-- Please describe in detail how you tested your changes -->
<!-- Include details of your testing environment, and the tests -->
<!-- you ran to see how your change affects other areas of the code,
etc. -->
<!-- This information is helpful for reviewers and QA. -->

<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Refactoring or add test (improvements in base code or adds test
coverage to functionality)

<!-- Go over all the following points, and put an `x` in all the boxes
that apply -->
<!-- If there are no documentation updates required, mark the item as
checked. -->
<!-- Raise up any additional concerns not covered by the checklist. -->

- [ ] I ensured that the documentation is up to date
- [ ] I explained why this PR updates go.mod in detail with reasoning
why it's required
- [ ] I would like a code coverage CI quality gate exception and have
explained why

<!---TykTechnologies/jira-linter starts here-->

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17049" title="TT-17049"
target="_blank">TT-17049</a>
</summary>

|         |    |
|---------|----|
| Status  | In Test |
| Summary | Memory fragmentation and unbounded RSS growth in CoProcess
bridge |

Generated at: 2026-05-28 12:22:16

</details>

<!---TykTechnologies/jira-linter ends here-->
MFCaballero added a commit that referenced this pull request Jun 10, 2026
…h a bounded LRU cache to prevent memory leaks (#8239) (#8275)

$(gh pr view 8275 --json body -q .body)

---
Requested by: <@U03HH6WGUCC>
Trace: d11557f2bd7b4d3d7167883a74bfbb71
Generated with Visor AI Assistant
<!---TykTechnologies/jira-linter starts here-->

### Ticket Details

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17049" title="TT-17049"
target="_blank">TT-17049</a>
</summary>

|         |    |
|---------|----|
| Status  | Merge |
| Summary | Memory fragmentation and unbounded RSS growth in CoProcess
bridge |

Generated at: 2026-06-04 09:28:30

</details>

<!---TykTechnologies/jira-linter ends here-->

---------

Co-authored-by: Tyk Bot <[email protected]>
Co-authored-by: Florencia Caballero <[email protected]>
Co-authored-by: Leonid Bugaev <[email protected]>
MFCaballero added a commit that referenced this pull request Jun 10, 2026
…th a bounded LRU cache to prevent memory leaks (#8239) (#8274)

[TT-17049] Replace unbounded regex caches with a bounded LRU cache to
prevent memory leaks (#8239)

<!-- Provide a general summary of your changes in the Title above -->

## Description
# TT-17049 — benchmark results

Hot-path benchmarks for `github.com/TykTechnologies/tyk/regexp` and
`github.com/TykTechnologies/tyk/internal/httputil`, measured to validate
that bounding the regex caches with `expirable.LRU` did not regress
request-handling cost.

- **Run date:** 2026-05-21
- **Host:** Apple M1 Pro · 10 cores · 16 GB RAM · macOS 15.6.1
- **Toolchain:** host `go1.25.1 darwin/arm64`; benchmarks executed via
Tyk's local `go` wrapper inside a Docker container (`go1.25.9
linux/arm64`),
  which is why the bench output reports `goos: linux, goarch: arm64`.
- **Iterations:** `-count=10 -benchtime=1s` — gives real 95% CIs on
every
  row (the previous `-count=5` runs printed `± ∞ ¹`).

## What changed

Two independent wins compose:

1. **Bounded LRU swap** (`internal/cache.Cache` → `expirable.LRU`). On
its
   own this adds a small overhead from the LRU's recency-update mutex on
   every Get — visible as the small `Replace*`/`FindAll*` regressions
   above. Allocations stay identical.

2. **Removed defensive `re.Copy()`** in `cache.getRegexp`. The Copy was
added in 2018-06-06 (PR #1689), eight months before Go 1.12 (Feb 2019)
   made `*regexp.Regexp` safe for concurrent reads without copying. Once
   removed, every cache hit drops 1 allocation and 160 B of garbage, and
   net wall time on Compile/Match drops well below master.

The Copy was pre-existing on master, not added by TT-17049. The Go
deprecation tooling surfaced the opportunity.

## Methodology

```sh
# Master worktree:
git worktree add -f /tmp/tyk-master-bench master
cd /tmp/tyk-master-bench
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... > master.bench

# TT-17049 branch (includes new httputil bench):
go test -bench='^Benchmark' -benchmem -run='^$' -count=10 -benchtime=1s \
  ./regexp/... ./internal/httputil/... > branch.bench

benchstat master.bench branch.bench > benchstat.txt
```

## Results — master vs TT-17049

### Wall time

| Benchmark | Master (ns/op) | Branch (ns/op) | Δ | p |


|------------------------------------|---------------:|---------------:|-------------:|------:|
| `RegExpCompile` | 121.90 | **86.72** | **−28.86%** | 0.000 |
| `RegExpCompilePOSIX` | 123.05 | **74.70** | **−39.29%** | 0.000 |
| `RegExpMustCompile` | 124.90 | **87.91** | **−29.62%** | 0.000 |
| `RegExpMustCompilePOSIX` | 124.05 | **75.48** | **−39.15%** | 0.000 |
| `RegExpMatchString` | 193.90 | **164.50** | **−15.14%** | 0.000 |
| `RegExpMatch` | 195.00 | **166.30** | **−14.74%** | 0.000 |
| `RegExpString` | 0.5086 | 0.5130 | +0.87% (sub-ns) | 0.000 |
| `RegexpReplaceAllString` | 74.79 | 77.46 | +3.58% | 0.002 |
| `RegexpReplaceAllLiteralString` | 74.71 | 78.41 | +4.95% | 0.000 |
| `RegexpReplaceAllStringFunc` | 106.80 | 112.20 | +5.06% | 0.000 |
| `RegexpFindAllString` | 82.02 | 85.20 | +3.89% | 0.000 |
| `RegexpFindAllStringSubmatch` | 82.20 | 85.71 | +4.26% | 0.000 |
| `RegexpFindStringSubmatch` | 77.05 | 76.79 | ~ | 0.853 |
| **Geomean (comparable subset)** | | | **−12.89%** | |

`~` = no statistically significant change (p > 0.05).

### Memory

| Benchmark | Master (B/op) | Branch (B/op) | Δ | p |


|--------------------------|--------------:|--------------:|------------:|------:|
| `RegExpCompile` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpCompilePOSIX` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMustCompile` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMustCompilePOSIX` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMatchString` | 176 | **16** | **−90.91%** | 0.000 |
| `RegExpMatch` | 176 | **16** | **−90.91%** | 0.000 |
| (others: ReplaceAll/Find)| 0 | 0 | — | — |
| **Geomean delta** | | | **−66.94%** | |

### Allocations

| Benchmark             | Master | Branch | Δ           | p     |
|-----------------------|------:|------:|------------:|------:|
| Compile / Match paths | 2     | **1** | **−50%**    | 0.000 |
| Replace / Find paths  | 0     | 0     | —           | —     |
| **Geomean delta**     |       |       | **−27.38%** |       |

## New benchmarks (branch-only)

These cover paths added by TT-17049 — no master comparator since the LRU
API didn't exist there.

| Benchmark                       | ns/op  | B/op  | allocs/op |
|---------------------------------|-------:|------:|----------:|
| `Cache_HitParallel`             | 164.4  | 0     | 0         |
| `Cache_MissAndAdd_Saturated`    | 670.4  | 160   | 4         |
| `Regexp_MatchString_Hit`        | 156.7  | 16    | 1         |
| `Regexp_Compile_Hit`            | 73.94  | 16    | 1         |
| `Regexp_Compile_Miss`           | 4136   | 5815  | 69        |
| `PreparePathRegexp_Hit`         | 164.0  | 47    | 2         |

### What these tell us

- **Cache hit is ~56× faster than a cold compile** (74 ns vs 4136 ns).
  Customer pattern sizes are ~70 KB vs the trivial `^abc.*$` benchmarked
  here, so production hit-vs-miss savings are even larger.

- **HitParallel at 164 ns / 0 allocs at GOMAXPROCS=10:** the
  `expirable.LRU` mutex is not pathological under concurrent reads.
  The plan's "P1 sharded-LRU contingency" remains unwarranted.

- **Saturated MissAndAdd at 670 ns/op:** if a workload's
distinct-pattern
  working set exceeds `RegexpCacheMaxEntries`, every miss costs
  `(670 + 4136) ≈ 4.8 µs` (eviction + cold compile). That's the
  5-minute eviction-summary warning's reason to exist.

- **`PreparePathRegexp_Hit` 2 allocs:** the `fmt.Sprintf` cache key,
  not the LRU itself. A future micro-opt (pooled `strings.Builder`)
  could remove these; not worth doing now.

## Why the Copy() removal works

`cache.getRegexp` previously returned `v.(*regexp.Regexp).Copy()` for
defensive concurrency. As of Go 1.12 (Feb 2019), the stdlib documents
`*regexp.Regexp` as safe for concurrent reads via its `Match*`, `Find*`,
`ReplaceAll*`, etc. methods without copying. Only `Longest()` mutates —
and Tyk's `Regexp.Longest()` wrapper is dead code (a logic-flipped
nil-guard that would only invoke the underlying `Longest()` on a nil
receiver, panicking).

The race tests in
`regexp/cache_test.go::TestCache_ConcurrentReads_SharedRegex`
hammer 14 read methods × 200 goroutines × 200 iterations on a shared
cached regex and pass cleanly under `go test -race`.

## Related Issue

<!-- This project only accepts pull requests related to open issues. -->
<!-- If suggesting a new feature or change, please discuss it in an
issue first. -->
<!-- If fixing a bug, there should be an issue describing it with steps
to reproduce. -->
<!-- OSS: Please link to the issue here. Tyk: please create/link the
JIRA ticket. -->

## Motivation and Context

<!-- Why is this change required? What problem does it solve? -->

## How This Has Been Tested

<!-- Please describe in detail how you tested your changes -->
<!-- Include details of your testing environment, and the tests -->
<!-- you ran to see how your change affects other areas of the code,
etc. -->
<!-- This information is helpful for reviewers and QA. -->

## Screenshots (if appropriate)

## Types of changes

<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Refactoring or add test (improvements in base code or adds test
coverage to functionality)

## Checklist

<!-- Go over all the following points, and put an `x` in all the boxes
that apply -->
<!-- If there are no documentation updates required, mark the item as
checked. -->
<!-- Raise up any additional concerns not covered by the checklist. -->

- [ ] I ensured that the documentation is up to date
- [ ] I explained why this PR updates go.mod in detail with reasoning
why it's required
- [ ] I would like a code coverage CI quality gate exception and have
explained why





<!---TykTechnologies/jira-linter starts here-->

### Ticket Details

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17049" title="TT-17049"
target="_blank">TT-17049</a>
</summary>

|         |    |
|---------|----|
| Status  | Merge |
| Summary | Memory fragmentation and unbounded RSS growth in CoProcess
bridge |

Generated at: 2026-06-03 12:45:37

</details>

<!---TykTechnologies/jira-linter ends here-->


[TT-17049]:
https://tyktech.atlassian.net/browse/TT-17049?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Co-authored-by: Florencia Caballero <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deps-reviewed Dependency changes reviewed and approved for CI execution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants