Simulate traffic patterns: peaks and bursts
How to simulate traffic patterns that rise and fall in a recurring rhythm, spike around a specific trigger, and stay quiet the rest of the time — using Eventum's time-pattern input instead of a flat cron or timer schedule.
A generator wired to a cron expression or a timer interval produces exactly the same number of events every tick, for as long as it runs. That flat rate is enough to check that a pipeline moves data from one end to the other, but nothing about it resembles real traffic: a load test built on it never has to absorb a real peak, and a detection rule tuned against it never sees a baseline it can deviate from, because every window of the stream looks exactly like the last one.
Eventum's time_patterns input plugin replaces that flat tick with a distribution: a repeating window, a baseline volume for that window, and a statistical shape that decides where inside the window each timestamp lands. The same mechanism builds a rhythm that rises and falls through a cycle and, layered a second time, a short and sharp spike on top of it — two techniques for the same goal: simulating traffic patterns instead of a flat, mechanical tick.
Why real traffic isn't flat
Production traffic follows the rhythm of whoever or whatever drives it. A consumer-facing API rises through business hours and falls back overnight; a batch pipeline sits quiet all day and then bursts the moment a scheduled job starts; a marketing email or an incident can push a spike through a system at a moment that has nothing to do with the time of day. None of this is noise to average away — it is the shape of the thing being tested.
That shape matters for two different reasons, depending on what the generated data stands in for. A load or capacity test exists to find out whether infrastructure survives the worst few minutes of a day, not the average minute — autoscalers, connection pools, and queues are only meaningfully exercised by an actual swing from quiet to busy, never by a flat, unchanging rate. A detection rule or an anomaly score is calibrated against a baseline it expects to deviate from. A flat stream gives it none: either nothing ever looks anomalous, or every small fluctuation does.
The same rhythm often repeats on a longer cycle too, since a weekday's traffic rarely matches a weekend's, though the techniques below focus on the daily cycle most systems feel first.
Simulate a diurnal pattern
time_patterns builds a distribution from four stages, each configured in a pattern file separate from the generator itself:
- Oscillator — divides time into a repeating window.
- Multiplier — sets a baseline number of timestamps per window.
- Randomizer — adds variance so no two windows produce exactly the same count.
- Spreader — places each window's timestamps inside it according to a statistical distribution.
A beta spreader with equal shape parameters concentrates timestamps around the middle of the window and thins them out toward both edges — a symmetric bell centered on the window — the shape a business-hours peak traces across a day. The pattern file below models a day-long window this way: a baseline of 2,400 requests, up to 15% more or less from one day to the next, clustered toward the middle of each day and quiet at the edges.
label: daily-traffic
oscillator:
start: "now"
end: "+3d"
period: 1
unit: days
multiplier:
ratio: 2400
randomizer:
deviation: 0.15
direction: mixed
spreader:
distribution: beta
parameters:
a: 4
b: 4- oscillator — a one-day window, repeated for three days from the moment the generator starts.
- multiplier — 2,400 requests as the baseline count for each day.
- randomizer — up to ±15% variance, so no two days produce an identical total.
- spreader — a beta distribution with equal shape parameters (
a: 4,b: 4), a symmetric bell that peaks at the middle of each day and tapers off at both edges.
That is how to simulate peak traffic without discarding the quiet hours around it — one distribution produces both from a single configuration.
Layer a bursty traffic spike on top
A single pattern file models one rhythm. time_patterns accepts more than one, merging every pattern's timestamps into the same chronological stream. That is what puts a short, high-volume trigger on top of the daily rhythm instead of replacing it. The pattern file below adds a ten-minute spike unrelated to the time of day, the kind a marketing blast or a scheduled batch job produces:
label: traffic-spike
oscillator:
start: "+1d3h"
end: "+10m"
period: 10
unit: minutes
multiplier:
ratio: 900
randomizer:
deviation: 0.0
direction: mixed
spreader:
distribution: uniform
parameters:
low: 0.0
high: 1.0start places the spike a day and three hours into the run, inside the quiet overnight stretch of the second day's cycle — one of the hours where the daily pattern alone produces its lowest counts. end is relative to this pattern's own start, not to the current time, so +10m is always a ten-minute window regardless of when the generator actually starts — see date ranges and versatile datetime for how every relative expression resolves. A uniform spreader with no randomizer deviation packs exactly 900 requests evenly across those ten minutes, with no bell shape softening the edges — a spike is meant to look abrupt.
The generator config
Both pattern files feed the same time_patterns input; a template renders each timestamp as one API request line using module.rand, and a file output writes the result through the json formatter:
{%- set path = module.rand.choice(["/api/v1/products", "/api/v1/cart", "/api/v1/checkout", "/api/v1/search"]) -%}
{%- set status = module.rand.weighted_choice({200: 92, 404: 5, 500: 3}) -%}
{"timestamp": "{{ timestamp.isoformat() }}", "path": "{{ path }}", "status": {{ status }}}input:
- time_patterns:
patterns:
- patterns/daily-traffic.yml
- patterns/traffic-spike.yml
event:
template:
mode: all
templates:
- request:
template: templates/request.jinja
output:
- file:
path: output/events.jsonl
write_mode: overwrite
formatter:
format: jsonSample mode (--live-mode false) generates all three simulated days at once for inspection — the result below reflects that run. A generator left running normally uses live mode instead, pacing the same distribution against the real clock, with the oscillator's end set to never rather than +3d to keep repeating the daily cycle indefinitely.
The result
Running the generator above for three simulated days produced 7,346 requests in total — 2,138 on Day 1, 3,103 on Day 2, and 2,105 on Day 3. Counting how many land in each three-hour slice of each day shows the shape the configuration describes rather than just asserts it:
| Hours into the day | Day 1 | Day 2 | Day 3 |
|---|---|---|---|
| 0-3 | 16 | 12 | 12 |
| 3-6 | 142 | 1,045 | 128 |
| 6-9 | 356 | 381 | 360 |
| 9-12 | 521 | 513 | 533 |
| 12-15 | 556 | 575 | 560 |
| 15-18 | 389 | 402 | 385 |
| 18-21 | 137 | 160 | 116 |
| 21-24 | 21 | 15 | 11 |
Day 1 and Day 3 carry the daily pattern alone: quiet in the first and last few hours of the cycle, rising through the following slices, peaking around the middle of the day, and falling back — the same shape twice, from two independent draws of the same configuration. Day 2 repeats that shape in every slice except one: the 3-to-6-hour slice jumps to 1,045, seven to eight times its counterpart on the other two days, because the ten-minute burst window falls inside exactly that slice.
Zooming into that ten-minute window itself, 903 of the slice's 1,045 requests land inside it — 900 from the burst pattern, plus a handful the daily pattern would have produced there on its own. Three consecutive lines from the quiet edge of Day 1, minutes apart:
{"timestamp": "2026-07-11T20:58:01.041093+00:00", "path": "/api/v1/search", "status": 200}
{"timestamp": "2026-07-11T21:04:36.927569+00:00", "path": "/api/v1/search", "status": 200}
{"timestamp": "2026-07-11T21:10:03.832763+00:00", "path": "/api/v1/cart", "status": 200}Three consecutive lines from inside the Day 2 burst, a second or a fraction of a second apart:
{"timestamp": "2026-07-12T22:25:21.399336+00:00", "path": "/api/v1/products", "status": 200}
{"timestamp": "2026-07-12T22:25:22.440915+00:00", "path": "/api/v1/products", "status": 200}
{"timestamp": "2026-07-12T22:25:22.591498+00:00", "path": "/api/v1/cart", "status": 200}FAQ
Related
- The Realism pillar for the other axes that make synthetic data behave like production traffic
- The Modeling sessions lesson for turning these timestamps into a connected sequence of actions instead of independent events
- The Realistic values lesson for shaping what each event contains, not just when it happens
- The Stream synthetic data to your stack pillar for what a realistic arrival rate means for the backend receiving it
- The time_patterns reference for every oscillator, multiplier, randomizer, and spreader parameter
- Ready-made generators in the Eventum Hub — every one currently ticks on a flat
cronschedule, each a candidate for the sametime_patternstreatment shown here
Realistic test data: behaving like production
Why uniform random output looks synthetic, and the techniques that make it behave like production traffic.
Simulate user sessions: state machines
How to simulate user sessions — a login, a stretch of activity, and a logout that share a session id and happen in order — using Eventum's finite state machine picking mode and per-session state.