This guide documents the load testing infrastructure, performance test scenarios, latency budgets, threshold definitions, and regression analysis procedures for the Agri-Fi platform.
The primary test script is maintained at tests/load/k6-performance.js.
Agri-Fi combines traditional REST API services with asynchronous task processing (RabbitMQ) and blockchain settlements (Stellar Horizon & Soroban smart contracts). Under peak demand—such as new seasonal harvest deal launches or bulk escrow milestone payouts—the system must handle concurrent user access without degrading response times or dropping transactions.
Load testing with k6 provides:
- Continuous Performance Benchmarks: Verifying that API endpoints meet predefined Service Level Objectives (SLOs).
- Regression Detection: Catching latency spikes, memory leaks, and database connection pool exhaustion before production releases.
- CI/CD Quality Gates: Enforcing automated threshold checks in CI workflows where performance regressions fail the pipeline.
- Install k6 locally (
brew install k6,winget install k6, or download standalone binary). - Ensure the backend API is up and running (default:
http://localhost:3001). - Ensure PostgreSQL, Redis, and RabbitMQ services are healthy (
docker compose up -d).
Run the load test against the default local instance:
k6 run tests/load/k6-performance.jsIf you do not have k6 installed natively, run via the official Docker image:
docker run --rm -i \
-v "$(pwd)/tests/load:/tests/load" \
--network="host" \
grafana/k6 run /tests/load/k6-performance.jsThe script accepts several environment variables for tuning virtual user counts, test duration, credentials, and target hosts:
| Variable | Description | Default |
|---|---|---|
BASE_URL |
Target API origin | http://localhost:3001 |
DEAL_ID |
Fallback deal UUID used if marketplace listing is empty | b0000000-0000-0000-0000-000000000001 |
INVESTOR_EMAIL |
Investor user credentials for login | investor@agri-fi.demo |
INVESTOR_PASSWORD |
Investor account password | Password123! |
TRADER_EMAIL |
Trader credentials for milestone triggering | trader@agri-fi.demo |
TRADER_PASSWORD |
Trader account password | Password123! |
VUS |
Concurrency for marketplace browsing scenario | 40 |
DURATION |
Duration for constant-VU scenarios | 30s |
INVESTMENT_VUS |
Peak concurrency for investment creation | 12 |
PAYOUT_VUS |
Peak concurrency for escrow payout triggers | 6 |
Example: Running an extended test against a staging environment:
k6 run \
-e BASE_URL=https://api-staging.agri-fi.com \
-e VUS=80 \
-e INVESTMENT_VUS=25 \
-e PAYOUT_VUS=10 \
-e DURATION=5m \
tests/load/k6-performance.jsThe test script defines three distinct concurrent scenarios using k6 multi-scenario execution:
┌─────────────────────────────────────────┐
│ k6 Load Execution │
└───────────────────┬─────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│marketplace_deals │ │investment_crea...│ │ escrow_payout │
│40 Constant VUs │ │Ramping VUs (2-12)│ │Ramping VUs (1-6) │
│List & Detail │ │Invest & Fund │ │Importer Milestone│
└──────────────────┘ └──────────────────┘ └──────────────────┘
- Executor:
constant-vus - VUs: 40 concurrent virtual users
- Duration: 30 seconds (configurable via
DURATION) - Simulated Flow:
- Requests
GET /v1/trade-deals?page=1&limit=12(tagged asListOpenDeals). - Extracts an active deal ID from the response payload.
- Requests
GET /v1/trade-deals/:id(tagged asGetDealDetail). - Pauses with a small jitter sleep (100ms) before the next iteration.
- Requests
- Goal: Simulates baseline traffic of retail and institutional investors browsing live agricultural listings, filtering commodities, and viewing deal prospectuses.
- Executor:
ramping-vus - Stages:
30s: Ramp up from 2 to 12 VUs.2m00s: Steady peak load at 12 VUs.30s: Ramp down to 6 VUs.
- Simulated Flow:
- Uses the authenticated investor JWT generated in the
setup()lifecycle hook. - Fetches open deals and constructs dynamic token investment orders.
- Calls
POST /v1/investmentswith variable lot sizes (tagged asCreateInvestment). - Follows up with
POST /v1/investments/:id/fundcontaining signed mock Stellar transaction envelopes (tagged asFundEscrow).
- Uses the authenticated investor JWT generated in the
- Goal: Tests write performance, relational constraints, transaction isolation levels, and token inventory decrement logic under concurrency.
- Executor:
ramping-vus - Stages:
20s: Ramp up from 1 to 6 VUs.2m00s: Steady load at 6 VUs.20s: Ramp down to 1 VU.
- Simulated Flow:
- Uses authenticated trader JWT.
- Submits
POST /v1/shipments/milestoneswithmilestone: "importer"(tagged asRecordImporterMilestone). - Verifies response payload for payout queue dispatch.
- Goal: Exercises final settlement logic, background RabbitMQ outbox publishing, and multi-party payout calculations (98% farmer, proportional investor returns, 2% platform fee).
Agri-Fi enforces hard thresholds configured directly in the k6 script options. If any threshold is breached, k6 immediately marks the run as failed and exits with non-zero code 99.
| Metric / Tag | Threshold | SLO Rationale |
|---|---|---|
http_req_failed |
< 0.05 (< 5%) |
Overall error rate (4xx/5xx) must remain under 5% under burst load. |
http_req_duration{name:ListOpenDeals} |
p(95) < 250ms |
Cached marketplace catalog read latency must return in under 250ms at 95th percentile. |
http_req_duration{name:GetDealDetail} |
p(95) < 250ms |
Deal metadata and milestone overview retrieval must return in under 250ms at 95th percentile. |
http_req_duration{name:CreateInvestment} |
p(95) < 1500ms |
Deal booking with DB transaction and row-level checks must finish under 1.5s at p95. |
http_req_duration{name:FundEscrow} |
p(95) < 2000ms |
Escrow verification, cryptographic signature validation, and queue dispatch must complete under 2.0s at p95. |
http_req_duration{name:RecordImporterMilestone} |
p(95) < 2000ms |
Shipment milestone registration and payout pipeline trigger must complete under 2.0s at p95. |
At the conclusion of a test run, k6 outputs a comprehensive metrics table. Here is how to parse and interpret each section:
✓ login returns 200
✓ list returns 200
✓ detail returns 200
✓ investment request handled
✓ http_req_duration..................................: avg=185.2ms min=12.1ms med=142.3ms max=2.1s p(90)=220ms p(95)=245ms
{ name:ListOpenDeals }...........................: avg=98.4ms min=14.2ms med=85.1ms max=310ms p(95)=195ms
{ name:GetDealDetail }...........................: avg=82.1ms min=11.5ms med=72.0ms max=280ms p(95)=180ms
{ name:CreateInvestment }........................: avg=640.2ms min=210ms med=580ms max=1.8s p(95)=1.1s
{ name:FundEscrow }..............................: avg=890.1ms min=320ms med=810ms max=2.4s p(95)=1.6s
{ name:RecordImporterMilestone }.................: avg=780.5ms min=290ms med=710ms max=2.2s p(95)=1.4s
✓ http_req_failed....................................: 0.12% ✓ 4 ✗ 3120
http_reqs..........................................: 3124 52.06/s
iteration_duration.................................: avg=1.2s min=410ms med=1.1s max=3.8s p(95)=2.1s
iterations.........................................: 2480 41.33/s
vus................................................: 58 min=1 max=58
vus_max............................................: 58 min=58 max=58
http_req_duration: Time spent making HTTP requests (includes DNS lookup, TLS handshake, data sending, waiting for server response [TTFB], and receiving content).p(95): The 95th percentile latency. 95% of all requests completed faster than this value. This is our primary Service Level Objective indicator.p(99)/max: Used to identify tail latency anomalies (e.g. garbage collection pauses or database connection pool acquisition waits).
http_req_failed: The ratio of failed requests (HTTP status >= 400 or network timeouts). A rate above 0.05 violates our threshold.checks: The proportion of application-level assertions passed (e.g., verifying that returned data is an array or deal IDs match).iterationsandvus: Total executed loop iterations and the concurrent virtual user count.
| Symptom | Probable Cause | Investigation Steps |
|---|---|---|
High http_req_failed (> 5%) |
Rate limiting / throttler activation or auth failure | Check app.module.ts throttler configuration (@nestjs/throttler), review server logs for 429 Too Many Requests. |
ListOpenDeals p95 > 250ms |
Redis cache miss or missing index on trade_deals(status, created_at) |
Verify Redis connection (REDIS_URL), check database query execution plan using EXPLAIN ANALYZE SELECT ... FROM trade_deals. |
CreateInvestment p95 > 1500ms |
Database row locking or connection pool exhaustion | Check DATABASE_POOL_MAX and DATABASE_POOL_CONNECTION_TIMEOUT_MS. Look for deadlock warnings in PostgreSQL logs. |
FundEscrow p95 > 2000ms |
Stellar Horizon RPC latency or RabbitMQ publish blockage | Check Horizon client response time, check RabbitMQ channel saturation and broker disk alarms. |
| Gradual latency increase over time | Memory leak or connection leak | Monitor container memory with docker stats or Grafana / Prometheus memory graphs (process_resident_memory_bytes). |
Export structured test results for CI reporting or regression tracking:
k6 run --summary-export=load-summary.json tests/load/k6-performance.jsStore baseline files in tests/load/baselines/ or as CI artifacts:
- When changes are introduced to database queries or critical business flows, run k6 and compare the resulting
http_req_duration.p(95)againsttests/load/baselines/baseline-latest.json. - A regression is flagged if any scenario's p95 latency exceeds 115% of the baseline without an approved architectural justification.
When k6 fails a threshold condition, it exits with status code 99. This property enables automated CI blocking:
In GitHub Actions workflows (e.g., .github/workflows/ci.yml and backend-ci.yml), load tests can be triggered as a scheduled nightly check or pre-release gate:
- name: Run k6 Load Test Suite
run: |
k6 run \
-e BASE_URL=http://localhost:3001 \
-e DURATION=45s \
--summary-export=backend/k6-summary.json \
tests/load/k6-performance.js
env:
DEAL_ID: "b0000000-0000-0000-0000-000000000001"If any endpoint exceeds its p95 budget (e.g., ListOpenDeals exceeding 250ms), the job will fail, preventing broken or regressed code from merging into main.
Performance budgets are strict commitments and must not be relaxed arbitrarily.
- Architectural Shifts: Introducing new asynchronous offloading mechanisms that intentionally trade initial sync latency for background processing.
- Infrastructure Spec Changes: Upgrades or resizing of PostgreSQL RDS instances or multi-AZ replica nodes.
- New Cryptographic / Compliance Requirements: Adding hardware security module (HSM) signing or multi-sig co-signers that measurably shift cryptographic processing time.
- Run a minimum of 5 consecutive load test iterations on a dedicated, staging environment with production-sized dataset.
- Calculate the mean p95 and p99 values across all 5 runs.
- Submit a Pull Request documenting:
- The proposed new thresholds in
tests/load/k6-performance.js. - The corresponding updates in this document.
- The test summary output and rationale.
- The proposed new thresholds in
- Require review and sign-off from Platform Engineering and Tech Lead before merging.