Open vs closed workload model: which one should you use?
7 min read
The open vs closed workload model question comes down to how users reach your system. Use an open model, which starts new iterations at a set arrival rate, for public websites and APIs where users keep coming however slow you are, and a closed model, a fixed number of virtual users that each wait for their last iteration to finish, only where the system itself caps concurrency. The choice matters most under saturation, where a closed test slows its own load down and makes the system look healthier than it is.
The tool facts below come from each project’s own documentation, checked on 9 October 2026.
| Open model | Closed model | |
|---|---|---|
| You set | New iterations or users per second | Concurrent virtual users |
| When responses slow down | Work keeps arriving and queues grow | Users wait, so less load is sent |
| Real systems like this | Most public websites and APIs | Call centres, ticket queues, worker pools |
| k6 | constant-arrival-rate, ramping-arrival-rate | constant-vus, ramping-vus |
| Gatling | injectOpen | injectClosed |
| Locust | Approximated with constant_throughput or poisson | Users with a wait_time |
| JMeter | Open Model Thread Group, Arrivals plugin | Thread Group |
What is the difference between an open and a closed workload model?
In an open model the load generator decides when work arrives, and in a closed model the system’s response time does.
The k6 documentation puts it plainly: in the closed model, a virtual user’s next iteration starts only when the last one finishes, while in the open model new iterations arrive independently of iteration completion (checked on 9 October 2026). A closed test with 50 virtual users never has more than 50 iterations in flight. An open test at 50 iterations per second keeps starting 50 every second, and if each one takes longer, more of them overlap.
Why does a closed model hide latency when the system saturates?
Because the test slows down together with the system. When responses take longer, each closed-model user sends fewer requests, so the load drops at the moment you most wanted to see it rise.
The k6 docs describe the effect: slower responses mean longer iterations and a lower arrival rate of new ones. Gatling’s page on workload models goes further and calls a closed model used against a system that is really open a broken test of an imaginary behaviour (checked on 9 October 2026).
A closed test therefore shows response times that level off, because requests in flight can never exceed the number of users. An open test shows the queue growing, with response times that climb for as long as arrivals outpace the system. That second picture is the one your users would see, and the p95 and p99 response times you read at the end are only as honest as the load model behind them.
The k6 docs note that some testing literature calls this problem coordinated omission. A 2006 USENIX paper that Gatling’s docs point to, Open Versus Closed: A Cautionary Tale, found that the two models give significantly different results even with the same load and service demands.
Which model matches your real traffic?
Pick an open model for anything the public can reach, and a closed one only where the system itself limits how many users are active at once.
Gatling’s docs name call centres where every operator is busy, and ticketing sites that queue users at full capacity, as closed systems, and say most websites behave as open ones. A batch job with a fixed number of workers or a queue consumer with set concurrency is closed too. Public APIs, web pages and webhooks are open: nothing holds back the next request because the last one was slow.
Many systems sit in between: a shopper arrives on their own schedule, then waits for each page within the visit. The paper above calls this a partly open model. In k6 or Gatling, set the arrival rate of sessions and make each iteration a whole user journey with its own pauses.
How do you set each model in k6?
You choose it with the executor. constant-vus and ramping-vus give you a closed model, while constant-arrival-rate and ramping-arrival-rate give you an open one.
A closed scenario sets executor: "constant-vus" with a number of vus and a duration. An open scenario sets a rate instead, and you pre-allocate enough virtual users to sustain it:
export const options = {
scenarios: {
open_model: {
executor: "constant-arrival-rate",
rate: 50,
timeUnit: "1s",
duration: "5m",
preAllocatedVUs: 100,
},
},
thresholds: {
dropped_iterations: ["count<10"],
},
};
k6 starts an iteration only while it has a free virtual user, and its docs advise against leaning on maxVUs, because adding users mid-test can overload the load generator. When no user is free, k6 records a dropped iteration, which late in a test often means the system is slowing down; the threshold above fails the run at ten. Leave sleep() out of the end of an arrival-rate iteration, since the executor already paces the starts (checked on 9 October 2026).
If you run k6 scripts on LoadFocus, its k6 cloud runner takes the load from the test’s own settings, and its script validator warns that arrival-rate scenarios run there with a fixed number of users, so the request rate is not held. For a true open-model test, run the arrival-rate executors with k6 itself, or on a service that runs them as written.
How do Gatling, Locust and JMeter express each model?
Gatling separates the two models in its API, JMeter offers both through different thread groups, and Locust is built around a fixed number of users.
Gatling. A scenario uses either injectOpen or injectClosed. Open steps such as constantUsersPerSec and rampUsersPerSec set arrival rates, and closed steps such as constantConcurrentUsers and rampConcurrentUsers set concurrency. One injection profile cannot mix the two (checked on 9 October 2026).
Locust. You set a number of users, and each user’s wait_time controls the pause between tasks. constant_throughput and constant_pacing adapt that pause so each user runs at most a set number of tasks per second, but the docs say wait time can only limit throughput and cannot start new users to reach a target. poisson() draws random pauses, which the docs call closer to real arrivals. Their example for 500 task iterations per second is constant_throughput(0.1) with 5,000 users, a closed pool with plenty of headroom (checked on 9 October 2026).
JMeter. The classic Thread Group is a closed pool of threads. The Constant Throughput and Precise Throughput timers pace that pool toward a rate, and the reference warns that throughput falls if the server cannot keep up or there are not enough threads. For arrivals, JMeter 5.5 added the Open Model Thread Group, still marked experimental, with schedules such as rate(10/sec) random_arrivals(1 min). The community Arrivals Thread Group plugin starts new threads when all existing ones are busy, with a concurrency limit as a safety valve (checked on 9 October 2026).
What should you watch in an open-model test?
Watch the load generator as closely as the system under test. In an open model, a slow system means more concurrent users on your side, which cost memory and CPU.
Set a ceiling, such as maxVUs in k6 or the plugin’s concurrency limit in JMeter, and treat reaching it as a result: the system could not absorb the rate you asked for.
FAQ
Is a closed workload model ever the right choice? Yes. Use it when the real system caps concurrency, such as a fixed pool of workers, a queue consumer or a call centre, or when you want to know how many simultaneous sessions a component can hold. For public web traffic an open model is the better fit.
Does adding think time turn a closed model into an open one? No. Think time lowers how often each user sends requests, but every user still waits for the previous response, so a slow system still throttles the test. Only an arrival rate set independently of response times gives you an open model.
How many virtual users does an arrival-rate test need? Enough to cover every iteration in flight at the same time. The k6 documentation suggests the median iteration duration multiplied by the target rate, plus a margin for variance, found through short trial runs before the full test.
Related reading
- Coordinated omission: what it is and how to avoid it
- Little’s law in load testing: size and check your tests
- Think time in load testing: what it is and how to set it
- Throughput vs latency: how they interact under load
- Amdahl’s law: formula, examples and scaling limits
- Queueing theory: what it is and why latency spikes
The technical claims in this article have not been independently reviewed.