Skip to main content
Aug 27, 2026k6 ramping arrival rate

By Lucas Yoris · Performate

Ramping Arrival Rate vs Open vs Closed Models: Picking an Executor for APIs

Ramping arrival-rate executors in k6: open vs closed workloads, maxVUs, dropped iterations—Grafana executor docs for API traffic models.

Your dashboard shows 500 requests per second at peak—but your k6 script holds 500 virtual users in a closed loop with no think time. Those are not the same question. Closed models cap concurrency; open models cap arrival rate. Mix them up and you either under-test the API or drown staging in VUs that never match how traffic actually arrives.

Choosing an executor is choosing a workload model. In this guide you will learn the difference between open and closed systems, when ramping-arrival-rate beats ramping-vus, and how to set maxVUs and stages so dropped iterations become a signal—not a silent failure.

Why executor choice changes the answer you get

API traffic from mobile apps, web clients, and partner integrations usually arrives as requests over time, not as a fixed pool of users looping as fast as possible:

  • Closed models (constant-vus, ramping-vus) keep N users active; throughput rises when responses get faster and falls when the API slows—classic "users waiting on the UI" behavior.
  • Open models (constant-arrival-rate, ramping-arrival-rate) target iterations or requests per second regardless of response time—closer to job queues, webhooks, and stateless API gateways.
  • Ramping stages expose knee points in autoscaling, connection pools, and rate limiters that flat VU counts miss.
  • Dropped iterations in open models mean the test could not spawn enough VUs to meet the target rate—a capacity signal for your test harness, not necessarily the SUT.

Think of closed vs open like a restaurant: fixed tables (closed) vs a line that keeps admitting guests at a steady pace (open). Slow kitchen service affects both—but the queue dynamics differ.

When VU ramps mislead API teams

If product asks "can we handle 200 req/s during the sale?", a ramping-vus test answers "how does latency look as we add users?"—not the same thing. Pair executor choice with think time and concurrency when closed models must mimic logged-in sessions, and with k6 scenario types when you need smoke vs stress vs soak shapes.

Practical k6 implementation: ramping-arrival-rate with stages

Model a sale ramp: start at baseline RPS, step through stages, and watch when latency or dropped iterations diverge.

Example script (illustrative—not a production-ready test). Fictional URLs and SLO numbers—adapt stages, rates, and maxVUs to your staging capacity.

What this example demonstrates:

  • Open workload: ramping-arrival-rate targets iterations per second, not a fixed VU count.
  • Multi-stage ramp: four stages from 10 → 40 → 80 → 120 iter/s over nine minutes—surfaces scaling knees.
  • Safety ceiling: maxVUs: 200 caps harness concurrency; raise it when k6 reports dropped iterations.
  • Route tags + thresholds: segment http_req_duration by route:search for SLO gates during the ramp.
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = __ENV.API_BASE || 'https://staging.example.com';

export const options = {
  scenarios: {
    sale_ramp: {
      executor: 'ramping-arrival-rate',
      startRate: 10,
      timeUnit: '1s',
      preAllocatedVUs: 30,
      maxVUs: 200,
      stages: [
        { target: 10, duration: '2m' },  // baseline
        { target: 40, duration: '2m' },  // early sale
        { target: 80, duration: '2m' },  // peak window
        { target: 120, duration: '3m' }, // hold peak
      ],
      tags: { scenario: 'sale_ramp', route: 'search' },
      exec: 'search',
    },
  },
  thresholds: {
    'http_req_duration{route:search}': ['p(95)<800', 'p(99)<1500'],
    http_req_failed: ['rate<0.02'],
    dropped_iterations: ['count==0'], // alert when harness cannot keep pace
  },
};

export function search() {
  const res = http.get(`${BASE}/search?q=laptop`, {
    headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
    tags: { route: 'search' },
  });
  check(res, { 'search 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.2); // light pacing; rate is still arrival-driven
}

Patterns that work

  • Start with preAllocatedVUs near expected need; increase maxVUs when summaries show dropped iterations (executors reference).
  • Hold stages long enough for autoscaling (often 2–5 minutes per step)—compare p95 vs p99 at each plateau.
  • Separate scenarios for read-heavy vs write-heavy routes when rates differ—avoid one ramp masking a hot endpoint.
  • Env-driven peaks: PEAK_RPS in the final stage lets CI run a low ramp while nightly jobs hit full sale targets.

Anti-patterns to avoid

  • Using ramping-vus when stakeholders asked for requests-per-second capacity.
  • Ignoring dropped_iterations—your test stopped asking hard questions.
  • Single 30-second ramp stages on cold clusters—false "pass" before caches and scale-out settle.
  • Setting maxVUs equal to preAllocatedVUs on aggressive ramps—k6 throttles rate silently.

Pro tip (example command): surface dropped iterations and tail latency in one summary.

k6 run sale-ramp.js --summary-trend-stats="p(95),p(99)" -e API_BASE=https://staging.example.com

What this command demonstrates: percentile trends plus threshold failures on dropped_iterations make executor misconfiguration visible in CI/CD load testing logs.

Decision framework: which executor when

SituationRecommended action
Stakeholder question is "N req/s at peak"ramping-arrival-rate or constant-arrival-rate with documented stages
Logged-in users clicking as fast as the UI allowsramping-vus + realistic think time
Finding breaking point (stress test)Ramp until errors or SLO breach; note whether open or closed matches prod
CI smoke after deployLow constant-arrival-rate (e.g. 5–10 iter/s), strict error thresholds
Soak / stability over hoursModerate constant-arrival-rate; watch memory and soak patterns

Use ramping-arrival-rate if traffic arrives independently of response time—API gateways, mobile backends, partner feeds.

Use ramping-vus if concurrency limits matter more than arrival rate—session pools, websocket rooms, licensed seats.

Use constant-arrival-rate if you need a flat regression baseline after you have validated ramp behavior once.

Observability, documentation, and next steps

Executor choice should be documented beside results—not inferred from chart shape later:

  • Record open vs closed rationale and link to the product/analytics question it answers.
  • Log stage targets, durations, maxVUs, and any dropped iterations per run.
  • Correlate ramp timestamps with autoscaling events and canary metrics if applicable.
  • Set alerts when p(99) diverges more than agreed epsilon between ramp plateaus.
  • Archive executor JSON and env overrides per release for apples-to-apples regression compares.

How Performate simplifies executor selection and ramps

Editing executor blocks by hand every time product changes the sale forecast does not scale. Below is a concrete workflow example for the same search API ramp this article discusses.

Example: build a sale ramp without memorizing executor syntax

  1. Import search requests from Postman or OpenAPI into one project. Problem solved: one request definition for manual QA and load tests.
  2. Choose "Ramping arrival rate" in the scenario editor and enter stage targets (10 → 40 → 80 → 120 iter/s) with hold durations. Problem solved: visual stages instead of trial-and-error stages arrays.
  3. Set preAllocatedVUs and maxVUs in the panel; run once and adjust when the report flags dropped iterations. Problem solved: harness capacity tuning without reading k6 source.
  4. Apply tags (route:search, scenario:sale_ramp) for report filters. Problem solved: stakeholders see the same segments engineers threshold in code.
  5. Compare runs side by side after infra changes—did the knee move from 80 to 100 iter/s? Problem solved: release reviews use one comparison view, not screenshot folders.
  6. Export the k6 script for nightly CI (load testing in CI/CD) with the same stages product signed off in the UI. Problem solved: pipeline and desktop runs stay aligned.

That workflow maps directly to the cta in this post: runnable scenarios and shareable reports without executor glue code every sprint.

Closing takeaway

Executors encode questions. Open arrival-rate ramps answer throughput and scaling knees; closed VU ramps answer concurrency and session behavior. Pick the model that matches how traffic arrives, watch dropped iterations, and hold stages long enough for autoscaling to tell the truth.

Replay this week's peak RPS target against staging with a ramping-arrival-rate scenario before the next sale—and note which stage first broke your p99 SLO.

Try Performate free | Book a demo | k6 executors

Ready to optimize your API performance?

Use Performate to turn this playbook into runnable k6 scenarios, thresholds, and shareable reports without losing days to glue code.

← Back to all posts