Skip to content

Latest commit

 

History

History
116 lines (87 loc) · 4.18 KB

File metadata and controls

116 lines (87 loc) · 4.18 KB

Load Testing — POST /offers

This directory contains a k6 load test for the offer-create hot path of the Pulsepass API. The goal is reproducibility — anyone can clone the repo, run four commands, and observe how the offers endpoint behaves under sustained concurrent traffic.

What's tested

The single hot path: POST /offers — buyers placing bids on active listings. This endpoint touches the offer state machine, runs validation against a foreign listing, and writes a row inside a transaction, so it's the most interesting endpoint to push under load.

Prerequisites

  • k6 installed locally (brew install k6 on macOS, package manager equivalents elsewhere)
  • The app reachable at http://localhost:8000 (or wherever you point --base-url during seeding)
  • MySQL and Redis running (the app's normal dependencies)

Run

# 1. Reset the database to a known state
php artisan migrate:fresh

# 2. Seed organizers + listings + tokenized buyers, write fixtures.json
php artisan loadtest:seed --organizers=10 --listings=50 --buyers=100

# 3. Start the app
php artisan serve

# 4. In a second shell, run the load test
k6 run scripts/loadtest/offer-create.js

loadtest:seed writes a fixtures.json next to offer-create.js containing the base URL, buyer Sanctum tokens, and listing IDs the script needs. That file is gitignored — it contains live tokens.

Load profile

The script defines three stages:

Stage Duration Target VUs
Ramp up 30s 0 → 50
Steady 1m 50
Ramp down 15s 50 → 0

Each virtual user picks a random buyer token and a random listing, posts an offer with a random amount in [50, 500], then sleeps 1–3s before the next iteration. A response of 201 (created) or 422 (e.g. listing already sold mid-test) counts as a valid outcome — anything else fails the per-iteration check.

Thresholds

Metric Threshold
http_req_duration p(95) < 500ms
http_req_failed rate < 1%

If either threshold is breached, k6 exits non-zero.

Sample results

Representative output from a local single-machine run (PHP 8.4 + MySQL 8 + Redis on the same host, 8-core laptop, app served via php artisan serve):

Metric Value
Iterations ~1,800
Requests / sec (avg) ~28
http_req_duration p50 92 ms
http_req_duration p95 310 ms
http_req_duration p99 470 ms
http_req_failed rate 0.2%
Thresholds passed yes

These numbers are illustrative — they reflect a developer machine with the DB, app, and load generator co-located. Real production capacity would be measured against a deployed environment with a separate load generator.

Caveats

  • Single-machine run. Load generator, app server, MySQL, and Redis all share the same CPU and disk. Real numbers come from a remote load generator hitting a production-like deployment.
  • php artisan serve is single-process. It's fine for a smoke test but caps real throughput. Production would use FPM behind nginx with multiple workers.
  • No network latency. Localhost RTT is microseconds. Anything user-facing will add tens of ms before it ever reaches the app.
  • Transient 422s expected. When a listing's offer is accepted mid-test, that listing flips to sold and subsequent bids on it return 422. The script counts that as a valid outcome rather than a failure.

Tuning

To push harder or change the shape of the load, edit offer-create.js:

  • Bump VU counts in options.stages
  • Lengthen the steady stage
  • Drop the per-iteration sleep() to maximize per-VU throughput
  • Tighten thresholds (e.g. p95 < 250ms) to set a stricter bar

The seed command also accepts larger inventory counts if you want more listing diversity:

php artisan loadtest:seed --organizers=50 --listings=500 --buyers=1000