Generate Realistic Load Test Data for an API
Generate realistic load test data — a diverse, weighted mix of request payloads — and fire it at a REST API in sample mode to benchmark throughput and find its breaking point.
Load test data is the request stream a load test actually needs: a realistic mix of operations, identifiers, and body shapes, released at whatever rate the test calls for — never the same payload sent as fast as possible. A load test built on the wrong kind of data measures the wrong thing — how fast a target answers the same cached request a thousand times, not how it holds up against the shape of traffic a real deployment receives.
Build a generator that produces that stream: a burst of diverse API requests fired at a target endpoint as fast as possible, to benchmark throughput, uncover race conditions, and validate rate limiters — all without writing test scripts or capturing real traffic first.
Generating realistic load test data
Replaying one fixed request in a curl loop is the obvious way to put load on an endpoint, and it does generate traffic — but every iteration hits the same cache entry, the same validator branch, the same row in a database, because every iteration sends the exact same body. A target under that kind of load looks fast for the wrong reason: it keeps answering the same question, not the varied mix a real deployment actually receives.
Realistic load test data breaks that pattern on three axes: operation mix — a spread of reads and writes instead of one endpoint hammered in isolation; identifiers — a different user, record, or resource on every request, so caches see a realistic mix of hits and misses and lookups touch more than one row; and payload shape — field values drawn from distributions and weighted choices instead of one constant body, so a validator's optional-field branches, length checks, and type coercions all get exercised, not just the one path a fixed payload happens to take.
None of this needs a captured production sample. module.rand and module.faker generate a fresh, plausible value on every render, and weighted_choice shapes the mix of operations and values to match what a real deployment's traffic actually looks like — the same techniques the Realistic values lesson covers for log events, applied here to API requests instead.
Load testing with diverse payloads
The build below turns that theory into a concrete pattern: four templates, one per CRUD operation, mixed by chance weights that approximate a typical web application's read-heavy traffic — reads dominate, creates and updates follow, deletes stay rare. Every template draws its own identifiers and field values fresh per request, so the 5,000 requests in one run touch different pages, different records, different names and roles — not one payload sent 5,000 times.
That volume of realistic requests answers a different question than a pipeline correctness test does. Throughput testing — this lesson — asks how much traffic a target can sustain before latency climbs or requests start failing; pipeline testing asks whether one realistic event survives a specific transport and lands intact at the other end. A target can pass one and fail the other: an endpoint that holds up under thousands of requests a second can still silently reject a payload shape a correctness check would have caught, and a pipeline that handles a single well-formed event perfectly can still fall over the moment real concurrency hits it.
What you'll build
The generator uses:
- static input — produces a burst of N timestamps instantly.
- Sample mode (
--live-mode false) — releases all timestamps without waiting for the clock. - chance picking mode — weighted mix of request types (GET, POST, PUT, DELETE).
- HTTP output — sends each event as a request to the target API; see Send test data to an API endpoint for batching and delivery mechanics in depth.
- Batch tuning — controls how many requests are in flight at once.
Prerequisites
- Eventum installed
- A target API endpoint to test (this tutorial uses a placeholder URL — replace it with your own)
Only load-test APIs you own or have explicit permission to test. Sending high-volume traffic to third-party services may violate their terms of service.
Project structure
Build it
Create the project directory
mkdir -p load-test/templates
cd load-testWrite request templates
Each template produces a JSON request body. The HTTP output sends it as a POST to the target endpoint. The template includes the HTTP method and path so the API can route accordingly (adjust the format to match your API's expectations).
GET users — list endpoint, no body needed. page is drawn from a clamped exponential distribution instead of a flat range, so most requests land on the first few pages and only a thin tail paginates deep — the way real API clients actually browse a list; limit is a weighted pick favoring the default page size over larger ones.
{
"method": "GET",
"path": "/api/v1/users",
"params": {
"page": {{ module.rand.number.clamp(module.rand.number.exponential(1 / 5), 1, 100) | round | int }},
"limit": {{ module.rand.weighted_choice({10: 60, 25: 30, 50: 10}) }}
}
}Create user — POST with a realistic payload.
{
"method": "POST",
"path": "/api/v1/users",
"body": {
"name": "{{ module.faker.locale.en.name() }}",
"email": "{{ module.faker.locale.en.email() }}",
"role": "{{ module.rand.weighted_choice(["viewer", "editor", "admin"], [0.6, 0.3, 0.1]) }}",
"department": "{{ module.rand.choice(["engineering", "marketing", "sales", "support", "hr"]) }}"
}
}Update user — PUT with partial changes.
{
"method": "PUT",
"path": "/api/v1/users/{{ module.rand.number.integer(1, 10000) }}",
"body": {
"name": "{{ module.faker.locale.en.name() }}",
"role": "{{ module.rand.choice(["viewer", "editor", "admin"]) }}"
}
}Delete user — DELETE by ID.
{
"method": "DELETE",
"path": "/api/v1/users/{{ module.rand.number.integer(1, 10000) }}"
}Every template above draws its identifiers, names, and roles fresh per render, so the batch below produces a genuinely varied stream instead of one fixture repeated 5,000 times.
Configure the generator
The static input generates 5,000 timestamps at once. The chance mode distributes requests across the four types with a realistic CRUD mix.
input:
- static:
count: 5000
event:
template:
mode: chance
templates:
- get-users:
template: templates/get-users.jinja
chance: 0.50
- create-user:
template: templates/create-user.jinja
chance: 0.25
- update-user:
template: templates/update-user.jinja
chance: 0.15
- delete-user:
template: templates/delete-user.jinja
chance: 0.10
output:
- http:
url: "http://localhost:8080/api/v1/batch"
method: POST
success_code: 200
headers:
Content-Type: "application/json"
Authorization: "Bearer test-token"
formatter:
format: json-batchKey settings:
- static count: 5000 — the total number of requests to generate.
- chance distribution — 50% reads, 25% creates, 15% updates, 10% deletes — mimics a typical web application workload.
- chance values are relative weights — 0.50/0.25/0.15/0.10 need not sum to 1; only the ratio between them matters (see chance picking mode).
- json-batch formatter — groups events into a JSON array per batch, reducing HTTP round-trips.
Run it
Use eventum generate in sample mode with tuned batch settings:
eventum generate \
--path generator.yml \
--id load-test \
--live-mode false \
--batch.size 100 \
--max-concurrency 10Flags explained:
| Flag | Effect |
|---|---|
--live-mode false | Releases all 5,000 timestamps instantly — maximum throughput |
--batch.size 100 | Groups 100 events per batch before sending |
--max-concurrency 10 | Up to 10 concurrent requests in flight at once — one per batch, see below |
--max-concurrency limits concurrent write operations across all output plugins — here, that's the http plugin's batch writes. The json-batch formatter configured above turns each batch into a single HTTP request carrying the whole batch as one JSON array (see batch vs per-request delivery). With json-batch, one write is one request, so --max-concurrency 10 caps the run at 10 requests in flight — not 10 batches' worth of per-event requests. The 5,000 events split into 50 batches of 100, with up to 10 of those batches' requests in flight at once. Swap the formatter to plain json and each batch fans out into one request per event instead, multiplying --batch.size by --max-concurrency into the real ceiling (1,000 here). Adjust --batch.size and --max-concurrency together to find your API's breaking point without overshooting it by accident.
Monitor results
eventum generate runs a single generator with no application server behind it, so there's nothing to query while it runs — and it prints no summary when it exits, even at the highest verbosity (-vvvvv). What you can check depends on what you're looking for:
Payload diversity, before pointing at a real target. Swap the http block for stdout (keep the same json-batch formatter) and the exact batches that would have been sent print instead — useful for confirming the CRUD mix and field values look right before a single request reaches the target API.
No test API to point at yet? The http output creates its client lazily and never tests url at startup, so an unreachable endpoint doesn't stop the run. Every write just fails in the background, logged rather than raised, and at the default verbosity that happens quietly — pass -v if you want it to show. The stdout swap above works whether or not url resolves to anything real.
Delivery counters, against a real target. A one-shot eventum generate run has no running application to expose these from, so this needs the full app instead: list the generator in startup.yml and start it with eventum run. Once it's running, the same counters every output plugin tracks become available live:
curl -u eventum:eventum http://localhost:9474/api/generators/load-test/stats| Counter | What it tells you |
|---|---|
written | Requests the target accepted |
write_failed | Write attempts that failed outright |
format_failed | Events that failed to format before a request was even sent |
See the Get Generator Stats reference for the full response shape, or open Studio's Instance metrics panel for the same numbers without a curl call.
These counters aren't equally reliable for the http output used here. write_failed only increases when a write is cancelled outright — a write_timeout expiring before a batch finishes sending, for example — because the plugin otherwise handles every response itself and never raises on a bad status code or a timed-out request. And because json-batch ships a whole batch as one request, a rejected or dropped batch leaves both written and write_failed unchanged — nothing marks the failure. Watch written against the number of events you actually sent (5,000 in this run) and treat any shortfall as failed requests. Run with -v so those failures show up in the logs instead of going unrecorded.
Going further
- Ramp-up pattern — replace
staticwith linspace over a 5-minute window in live mode to gradually increase load. - Error injection — add a template that sends intentionally malformed requests (missing required fields, invalid types) to test API error handling.
- Multi-endpoint testing — run several generators in parallel via
startup.yml, each targeting a different API service. - Throughput measurement — pipe
stdoutoutput throughpv -lto count events per second in real time.
What's next
static reference
Burst timestamp generation for one-shot workloads.
HTTP output
URL, headers, auth, and concurrency settings.
Performance tuning
Batching, concurrency, and backpressure controls.
FAQ
Related
- The Send test data to an API endpoint lesson for HTTP delivery, batching, and authentication in depth
- The Realistic values lesson for the distributions and weighted choices behind a diverse payload
- The Test data pipeline lesson for confirming correctness instead of capacity
- The Scenarios track for how this scenario relates to the rest of the course
- The Eventum Hub for pre-built generators covering common API traffic patterns
Seed a Database with Realistic Test Data
Seed a database with realistic test data — generate a shaped e-commerce dataset of purchases, refunds, and chargebacks as CSV, ready to load into a dev or staging database instead of copying production.
Generate Clickstream Data for ClickHouse
Generate clickstream data — user browsing sessions and bounces modeled with a finite state machine — and stream page views into ClickHouse for funnel analysis.