Skip to content

Token Exchange Load Test

Test Overview

ItemDetails
Test DateDecember 14, 2025
Target EndpointPOST /token (grant_type=token-exchange)
PurposeMeasure performance for microservice authentication and SSO audience switching

Test Environment

K6 Cloud Configuration

ComponentDetails
Load GeneratorK6 Cloud (amazon:us:portland)
Targethttps://conformance.authrim.com
ProtocolClient Secret (Basic Auth)

Infrastructure

ComponentTechnology
ComputeCloudflare Workers (op-token)
Revocation CheckDurable Objects (TokenRevocationStore)
Key ManagementDurable Objects (KeyManager)
DatabaseCloudflare D1

Sharding Configuration

Durable ObjectShardsPurpose
AuthorizationCodeStore8Auth code management
SessionStore8Session management
ChallengeStore8Challenge management
TokenRevocationStore8Token revocation check (main DO)
RefreshTokenRotator16Refresh token management

Test Methodology

Token Mix (RFC 8693 + Industry Standard)

Token TypeRatioExpected ResultValidation
Valid (standard)56%New token issuedScope/sub integrity
Valid (with actor)14%New token issuedDelegation flow (RFC 8693)
Expired10%400 errorImmediate detection
Invalid signature10%400 errorSignature verification
Revoked10%400 errorReal-time revocation check

Token Exchange Variations

VariationTypesExamples
Target Audience20api.example.com/gateway, /users, /payments, …
Scope Patterns4openid, openid profile, openid profile email, full
Resource URI10resource.example.com/api/v1, data.example.com/graphql, …
Service Clients5service-gateway, service-bff, service-worker, …

Load Pattern

{
scenarios: {
warmup: {
executor: 'constant-arrival-rate',
rate: 50,
duration: '30s',
exec: 'warmupScenario',
},
token_exchange_benchmark: {
executor: 'ramping-arrival-rate',
startRate: 0,
timeUnit: '1s',
preAllocatedVUs: 3600,
maxVUs: 4500,
stages: [
{ target: 1500, duration: '15s' },
{ target: 3000, duration: '180s' },
{ target: 0, duration: '15s' },
],
startTime: '30s',
},
},
}

Test Configuration

RFC 8693 Request Format

POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token={access_token}
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience={target_audience}
&scope={requested_scope}

Success Criteria

  • Valid tokens → 200 with new token
  • Expired/Invalid/Revoked → 400 with error
  • 100% token validation accuracy

Results - Performance Metrics

Summary

RPSTotal RequestsK6 P95CF Worker P99CF DO P99Status
2,000292,343500ms307ms1,020ms⚠️
2,500365,624225ms313ms271ms
3,000390,4442,144ms316ms2,222ms

Criteria: ✅ K6 P95 < 300ms AND DO P99 < 500ms

K6 Client Latency (ms)

RPSMedianP95P99MinMax
2,000112500589403,448
2,50076225297395,075
3,0001,6572,1442,269534,236

RPS Achievement

Target RPSAvg RPSPeak RPSAchievement
2,0001,3732,137107%
2,5001,7172,494100%
3,0001,8332,71490%

Note: 3,000 RPS test had “Insufficient VUs” warning

Results - Infrastructure Metrics

Worker Duration (ms)

RPSTotal ReqP50P75P90P99P999
2,000293,74723.8326.0460.67306.87309.91
2,500367,48423.0924.5226.43312.86315.63
3,000398,732204.33236.41265.56315.89337.54

Worker CPU Time (ms)

RPSP50P75P90P99P999
2,0002.273.063.648.4716.59
2,5002.232.523.198.4415.89
3,0002.132.373.034.977.23

Key Finding: CPU time stable at ~2.3ms P50 - CPU is NOT the bottleneck

Durable Objects Wall Time (ms)

TokenRevocationStore + KeyManager DO:

RPSTotal DO ReqDO ErrorsP50P75P90P99P999
2,000620,073017.7629.88102.051,019.691,312.22
2,500764,279015.0828.6346.66271.45378.61
3,000759,0108759.341,512.101,821.782,222.332,450.69

Key Finding:

  • 2,500 RPS is the sweet spot (P99 271ms)
  • 3,000 RPS saturates DO (P50 jumps to 759ms)
  • DO errors begin at 3,000 RPS

D1 Database Metrics

RPSRead QueriesWrite QueriesRows ReadRows Written
2,0001,01061,01614
2,500810681614
3,0001,01061,01614

Note: Token Exchange only reads client info from D1 (high cache hit rate)

DO Call Parallelization Effect

Implementation

// Before: Sequential (2 RTT)
const publicKey = await getVerificationKeyFromJWKS(env, kid);
await verifyToken(...);
const revoked = await isTokenRevoked(env, jti);
// After: Parallel (1 RTT)
const [publicKey, revoked] = await Promise.all([
getVerificationKeyFromJWKS(env, kid),
isTokenRevoked(env, jti)
]);
await verifyToken(...);

Impact at 3,000 RPS

MetricBeforeAfterChange
DO P5049ms28ms-43%
DO P992,141ms2,222ms~same
Worker P99307ms316ms~same

P50 significantly improved. P99 unchanged due to queuing dominance at high load.

Capacity Recommendations

UsageRecommended RPSRationale
Normal Operation≤1,500Comfortable stable operation
Peak Handling≤2,500K6 P95 225ms, DO P99 271ms - optimal point
Absolute Limit≤2,700Peak RPS achievable

Key Findings

1. CPU Processing is Fast and Stable

CPU Time P50 at 2.1-2.3ms across all RPS - CPU is NOT the bottleneck.

2. DO is the Bottleneck

At 3,000 RPS, DO P50 jumps from 15ms to 759ms (50x degradation).

3. 2,500 RPS is the Sweet Spot

Best performance achieved at 2,500 RPS:

  • K6 P95: 225ms
  • DO P99: 271ms

4. 3,000 RPS Saturates the System

  • DO queuing delay dominates
  • Performance degrades rapidly
  • DO errors begin (8 errors)

5. 100% Token Validation Accuracy

All token types correctly validated:

  • Valid → New token issued
  • Expired → 400 error
  • Invalid signature → 400 error
  • Revoked → 400 error (real-time check)

2,000 RPS vs 2,500 RPS Anomaly

2,000 RPS showed worse P95 (500ms) than 2,500 RPS (225ms):

FactorExplanation
Timing2,000 RPS: 14:49 JST, 2,500 RPS: 16:43 (~2 hours later)
DO WarmupDOs were cold at 2,000 RPS test
VU Usage2,000 RPS: ~878 VU, 2,500 RPS: ~437 VU

Higher VU usage at 2,000 RPS indicates slower server responses (VUs waiting).

Conclusion: 2,500 RPS is still the stable upper limit. 2,000 RPS anomaly was due to cold DO state.

Architecture Diagram

flowchart TB
    subgraph Test["Test Environment"]
        k6["k6 Cloud (Portland, OR)"]
    end

    subgraph CF["Cloudflare Edge"]
        subgraph Worker["op-token Worker"]
            TE["Token Exchange Handler (RFC 8693)"]
            CA["Client Authentication"]
            STV["Subject Token Validation"]
            SI["Scope Intersection"]
            ATG["Access Token Generation"]
        end

        subgraph DO["Durable Objects (shared)"]
            KM["KeyManager (1)
JWK management, signing key"] TRS["TokenRevocationStore (8 shards)
Token revocation check"] end subgraph DB["Database"] D1["D1: Clients, Users"] end end k6 -->|HTTPS| TE TE --> CA CA --> STV STV --> SI SI --> ATG TE -->|"RPC Call (parallel)"| KM TE -->|"RPC Call (parallel)"| TRS TRS --> D1

Bottleneck Analysis

Layer2,000 RPS2,500 RPS3,000 RPS
K6 Client P95500ms ⚠️225ms ✅2,144ms ❌
Worker CPU P502.27ms ✅2.23ms ✅2.13ms ✅
Worker Duration P5023.83ms ✅23.09ms ✅204.33ms ⚠️
DO Wall Time P5017.76ms ✅15.08ms ✅759.34ms ❌
DO Wall Time P991,020ms ⚠️271ms ✅2,222ms ❌
VerdictVariabilityOptimalDegraded

Conclusion

Authrim’s Token Exchange (RFC 8693) endpoint achieves:

  • Up to 2,500 RPS: Stable operation (K6 P95 225ms, CF DO P99 271ms)
  • 3,000+ RPS: Visible degradation (K6 P95 > 2,100ms, CF DO P50 > 750ms)

Primary bottleneck is Durable Object queuing delay - not CPU or cryptographic operations. Further scale-out requires DO optimization or architecture changes (cache layer addition).

100% success rate achieved at all RPS levels - token validation accuracy is maintained even at throughput limits.

Key Finding (English Summary)

Authrim’s Token Exchange endpoint sustains 2,500 RPS under realistic service-to-service authorization workloads, with strict token validation and revocation checks enabled.

The observed upper limit is defined by Durable Object queueing, not CPU or cryptographic operations.

This benchmark includes:

  • Full JWT RS256 signature verification on every request
  • Real-time revocation checks against Durable Object storage
  • Mixed token types (70% valid, 10% expired, 10% invalid, 10% revoked)
  • Delegation flow testing (14% with actor_token)
  • Audience variation (20 different target audiences)
  • Scope downgrading (4 scope patterns)