Synkora Load Testing with k6

March 22, 2026 · View on GitHub

Load testing suite for Synkora API endpoints using k6.

⚠️ IMPORTANT: Avoid Real LLM API Costs

Before running load tests, start the API server with LOAD_TEST_MODE=true to use mock LLM responses instead of making real API calls:

# Option 1: Docker Compose (recommended)
LOAD_TEST_MODE=true docker-compose up -d api

# Option 2: Local development
cd api
LOAD_TEST_MODE=true uvicorn src.app:app --reload --port 5001

This enables a mock LLM provider that:

  • Simulates realistic streaming responses with delays
  • Exercises the full request pipeline (auth, rate limiting, RAG, etc.)
  • Does NOT call OpenAI, Anthropic, or any real LLM API
  • Costs $0 regardless of test volume

To disable mock mode and use real LLMs again:

# Docker Compose
docker-compose up -d api   # LOAD_TEST_MODE defaults to false

# Or explicitly
LOAD_TEST_MODE=false docker-compose up -d api

Prerequisites

# Install k6
# macOS
brew install k6

# Ubuntu/Debian
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

# Docker
docker pull grafana/k6

Quick Start

1. Get Auth Token

# Login to get JWT token
curl -X POST http://localhost:5001/console/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@example.com", "password": "password"}'

2. Run Smoke Test (Quick Sanity Check)

# Health endpoints only (no auth required)
k6 run --env SCENARIO=smoke tests/load/main.js

# With authentication
k6 run --env SCENARIO=smoke \
  --env AUTH_TOKEN=<your-jwt-token> \
  --env AGENT_NAME=<your-agent-name> \
  tests/load/main.js

3. Run Load Test (Normal Traffic)

k6 run --env SCENARIO=load \
  --env AUTH_TOKEN=<your-jwt-token> \
  --env AGENT_NAME=<your-agent-name> \
  tests/load/main.js

4. Run Stress Test (Find Breaking Point)

k6 run --env SCENARIO=stress \
  --env AUTH_TOKEN=<your-jwt-token> \
  --env AGENT_NAME=<your-agent-name> \
  tests/load/main.js

5. Run Chat-Specific Stress Test

k6 run --env AUTH_TOKEN=<your-jwt-token> \
  --env AGENT_NAME=<your-agent-name> \
  --env MAX_VUS=100 \
  tests/load/chat-stress.js

Test Files

FileDescription
main.jsMulti-endpoint load test with traffic distribution
chat-stress.jsFocused stress test for chat streaming endpoint
config.jsShared configuration and test data generators

Environment Variables

VariableDefaultDescription
BASE_URLhttp://localhost:5001API base URL
AUTH_TOKEN-JWT authentication token
AGENT_NAMEtest-agentAgent name for chat tests
AGENT_ID-Agent ID for widget tests
KB_ID-Knowledge base ID for search tests
SCENARIOsmokeTest scenario (smoke/load/stress/spike/soak)
MAX_VUS50Max virtual users for chat-stress.js

Test Scenarios

Smoke Test

  • Duration: 30 seconds
  • VUs: 1
  • Purpose: Verify endpoints work correctly

Load Test

  • Duration: 9 minutes
  • VUs: Ramp 0 → 50 → 0
  • Purpose: Normal expected traffic

Stress Test

  • Duration: 13 minutes
  • VUs: Ramp 0 → 50 → 100 → 200 → 300 → 0
  • Purpose: Find breaking point

Spike Test

  • Duration: 5 minutes
  • VUs: 10 → 200 (sudden spike) → 10
  • Purpose: Test sudden traffic surges

Soak Test

  • Duration: 30 minutes
  • VUs: 30 (constant)
  • Purpose: Detect memory leaks

Known Limits

From codebase analysis:

ResourceLimitConfig Location
DB Pool75 + 50 overflow (125 total)api/src/config/database.py
Redis Connections200api/src/config/redis.py
WebSocket Per User10api/src/core/websocket.py
WebSocket Per Tenant500api/src/core/websocket.py
WebSocket Total10,000api/src/core/websocket.py
Vector DB Pool5 per configapi/src/services/performance/connection_pool.py

Rate Limits

Endpoint PatternLimit
/api/v1/agents/60 req/min
/v1/chat/30 req/min
/api/v1/files/upload20 req/min
/api/v1/data-analysis/upload10 req/min
/webhook/100 req/min
/health1000 req/min
Default100 req/min

Benchmark Results (March 2026)

Test environment: Single API instance, LOAD_TEST_MODE=true (mock LLM), Docker Compose.

Smoke Test (1 VU, 30s)

MetricValue
Success Rate100% (4/4 requests)
Checks Passed100% (13/13)
Failed Requests0%
Chat Stream p(95)10.62s
List Agents636ms

Load Test (50 VUs, 9 min)

MetricValue
Success Rate99.89% (4579/4584)
Checks Passed99.93% (13744/13753)
Failed Requests0.04% (2/4585)
Total Requests4585 (8.4 req/s)
Errors5
Endpointavgp(90)p(95)p(99)Threshold
Chat Stream11.16s13.77s14.29s15.9sp(95)<30s
List Agents487ms930ms979ms1.02sp(95)<2s
Health460ms925ms980ms1.2sp(95)<2s
Ready977ms1.01s1.02s1.11sp(95)<2s

All thresholds passed.

Stress Test (300 VUs, 13 min)

Connection pool exhaustion observed at peak concurrency (300 VUs). The QueuePool limit was reached, causing 500 errors on some requests. This is expected at the designed stress limit for a single instance.

Breaking point: ~200 concurrent VUs per single API instance. Beyond this, horizontal scaling (multiple pods) is required.

Notes

  • Chat stream durations include mock LLM response time (~10s simulated streaming delay)
  • Session isolation pattern uses multiple DB connections per chat request (main + prompt builder + parallel resource loading)
  • Redis caching (agent config, tools, context files) significantly reduces DB pressure under load
  • For production at scale, deploy multiple API pods behind a load balancer

Interpreting Results

Key Metrics

http_req_duration.............: avg=2.94s   p(95)=12.48s  p(99)=14.35s
http_req_failed...............: 0.04%
chat_stream_duration..........: avg=11.16s  p(95)=14.29s
success_rate..................: 99.89%
errors........................: 5

What to Look For

  1. http_req_duration p(95): Should be < 2s for most endpoints
  2. http_req_failed: Should be < 5% under normal load
  3. chat_stream_duration: Includes LLM response time, < 30s for p(95)
  4. rate_limited count: If high, rate limits may be too aggressive
  5. errors: Any consistent errors indicate bugs

Red Flags

  • p(99) >> p(95): Indicates outliers, possible resource exhaustion
  • Failed rate increasing with VUs: System hitting limits
  • TTFB increasing linearly with VUs: DB connection pool exhaustion
  • Consistent 5xx errors: Server-side issues
  • QueuePool timeout errors: Need larger pool or horizontal scaling

Grafana Integration

Export results to Grafana Cloud:

K6_CLOUD_TOKEN=<token> k6 cloud tests/load/main.js

Or output to InfluxDB:

k6 run --out influxdb=http://localhost:8086/k6 tests/load/main.js

CI/CD Integration

# .github/workflows/load-test.yml
name: Load Test
on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM
  workflow_dispatch:

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: grafana/k6-action@v0.3.1
        with:
          filename: api/tests/load/main.js
        env:
          SCENARIO: load
          BASE_URL: ${{ secrets.STAGING_URL }}
          AUTH_TOKEN: ${{ secrets.LOAD_TEST_TOKEN }}
          AGENT_NAME: load-test-agent