#8484·tyk

Add non-cloning ctx.GetOASDefinitionRaw accessor for read-only hot paths

Author: emmanuelchaplais2Created Jul 13, 2026Updated Sep 2, 2026
Labelsenhancementexternal

Is your feature request related to a problem? Please describe.

ctx.GetOASDefinition() returns a deep clone of the OAS definition stored in the request context, built through reflect.Clone. It clones on every call, even where the caller only reads the result.

In production, at around 100 requests per second, one auth plugin spends about 80% of its CPU time in that clone. It calls GetOASDefinition() once per request. That cost is a scaling bottleneck for a gateway instance under load.

mermaid
flowchart LR
    A[Request context] --> B{Caller needs to mutate?}
    B -->|No, read only| C[GetOASDefinitionRaw]
    B -->|Yes| D[GetOASDefinition]
    C --> E[Live *oas.OAS pointer<br/>0 allocations]
    D --> F[reflect.Clone<br/>about 75.5 KB, 857 allocations]

A benchmark against a realistic definition shows the cost. Timing varies by machine. The allocation counts do not:

Call Time Allocations Memory
GetOASDefinition (clone) 215 µs 857 75.5 KB
Direct pointer read 5 ns 0 0 B

The deep copy exists so a caller can safely mutate a request-private copy, without racing other concurrent requests. That safety matters for the general case, but a caller that only reads the definition pays that cost for no benefit.

Describe the solution you'd like

Add ctx.GetOASDefinitionRaw(r *http.Request) *oas.OAS. It returns the shared *oas.OAS pointer stored in the context, with no clone. This follows the "Raw" naming already used in the codebase for an unprocessed, underlying value, such as log.GetRaw() and certificateManager.GetRaw().

GetOASDefinitionRaw returns the live pointer. A caller must not mutate it. GetOASDefinition is unchanged and stays the safe default for a caller that needs to mutate the result.

The new function is additive. Existing code keeps calling GetOASDefinition and keeps the same guarantees.

Describe alternatives you've considered

  1. Change GetOASDefinition to return the raw pointer directly. Rejected: it breaks any caller that mutates the result today.
  2. Add a boolean parameter, such as GetOASDefinition(r, clone=true). Rejected in favor of two distinct functions, which reads more clearly at the call site and matches the existing "Raw" naming pattern.
  3. Clone lazily, only once the caller actually mutates the result. Rejected: detecting a mutation is itself expensive, and the two-function approach is simpler.
  4. Cache the clone per request. Rejected: cloning is already cheap to repeat, so a cache only helps a caller that calls GetOASDefinition more than once per request. That does not justify the added context-map state.

Additional context

The OAS definition is a 200 to 300 KB object. A full reflect.Clone walks and reallocates the whole structure, which becomes hot for any plugin or middleware that reads it once per request.

The benchmark ran as a standard Go benchmark (go test -bench=. -benchmem), with the iteration count chosen by the testing framework.

The classic-definition equivalent, ctx.GetDefinition(), has the same clone cost. That is a separate piece of work, not covered here. This issue and its PR stay scoped to the profiled OAS auth path.

AI assistance disclosure

I used an AI coding assistant (Claude) to draft this issue and the linked PR's code, tests, and description. I read and reviewed the diff and the reasoning behind it myself before opening either.