<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>loadtest.app</title><description>Load testing tools reviewed and compared, plus guides to running and reading load tests.</description><link>https://loadtest.app/</link><language>en</language><item><title>Amdahl&apos;s law: formula, examples and scaling limits</title><link>https://loadtest.app/amdahls-law/</link><guid isPermaLink="true">https://loadtest.app/amdahls-law/</guid><description>Amdahl&apos;s law explained with the formula, a speedup table and worked examples, plus Gustafson&apos;s law, the USL and what all three mean for load tests.</description><pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Amdahl&amp;#39;s law says that the speedup from running work in parallel is capped by the part that cannot run in parallel. If a fraction p of the work spreads across n processors, servers or threads, the speedup is S = 1 / ((1 − p) + p/n), and however large n grows it never passes 1 / (1 − p). With 95% of the work parallel, the ceiling is 20 times, even on a thousand machines.&lt;/p&gt;
&lt;p&gt;The numbers below are arithmetic, with no test run behind them. The papers cited were checked on 1 November 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parallel fraction p&lt;/th&gt;
&lt;th&gt;n = 2&lt;/th&gt;
&lt;th&gt;n = 8&lt;/th&gt;
&lt;th&gt;n = 64&lt;/th&gt;
&lt;th&gt;n → ∞&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;1.33&lt;/td&gt;
&lt;td&gt;1.78&lt;/td&gt;
&lt;td&gt;1.97&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;1.82&lt;/td&gt;
&lt;td&gt;4.71&lt;/td&gt;
&lt;td&gt;8.77&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;1.90&lt;/td&gt;
&lt;td&gt;5.93&lt;/td&gt;
&lt;td&gt;15.42&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;1.98&lt;/td&gt;
&lt;td&gt;7.48&lt;/td&gt;
&lt;td&gt;39.26&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What does the law say in simple terms?&lt;/h2&gt;
&lt;p&gt;A job can finish no faster than its serial part, so extra workers shrink only the rest. Once the parallel part is small next to the serial part, more workers barely change the total time.&lt;/p&gt;
&lt;p&gt;Take a 10-second job where 1 second is serial (a lock, a single write) and 9 seconds split evenly, so p = 0.9. Eight workers finish it in 1 + 9/8 = 2.125 seconds (4.71 times faster); sixty-four need about 1.14 seconds. Nothing gets it under 1 second, so 10 is the ceiling.&lt;/p&gt;
&lt;h2&gt;Where does the law come from?&lt;/h2&gt;
&lt;p&gt;Gene Amdahl made the argument in 1967, at the AFIPS Spring Joint Computer Conference, in a paper defending the single-processor approach.&lt;/p&gt;
&lt;p&gt;We have not read the full text of &lt;a href=&quot;https://doi.org/10.1145/1465482.1465560&quot;&gt;Validity of the single processor approach to achieving large scale computing capabilities&lt;/a&gt;. Mark Hill and Michael Marty&amp;#39;s &lt;a href=&quot;https://research.cs.wisc.edu/multifacet/amdahl/&quot;&gt;Amdahl&amp;#39;s Law in the Multicore Era&lt;/a&gt; (IEEE Computer, 2008) summarizes it: one fraction infinitely parallelizable with no scheduling overhead, the rest totally sequential, and the speedup described &amp;quot;without presenting an equation&amp;quot;.&lt;/p&gt;
&lt;h2&gt;What does it mean for load and capacity testing?&lt;/h2&gt;
&lt;p&gt;Throughput stops growing in step with servers, threads or pods once a shared component handles part of every request one at a time. A load test shows it as a throughput curve that flattens while you keep adding capacity.&lt;/p&gt;
&lt;p&gt;Neil Gunther&amp;#39;s 2008 paper, cited below, writes the same law as N / (1 + σ(N − 1)), with σ the serial fraction, and measures scalability as throughput at N divided by throughput at 1. In a web system, the serial part is whatever every request passes through in turn: a lock or a row every request updates, a single database primary taking all writes, a single queue consumer or a rate-limited third-party API.&lt;/p&gt;
&lt;h2&gt;How do you find the serial part in a load test?&lt;/h2&gt;
&lt;p&gt;Run the same test at several load levels and watch which resource saturates as throughput flattens. The component at its limit when extra capacity stops paying off is the serial part, or the queue in front of it.&lt;/p&gt;
&lt;p&gt;Typical signs: throughput levels off while the app tier has spare CPU, waits grow in one shared place (locks, database connections, queue depth), and response time rises in step with users, which is &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law&lt;/a&gt; at a ceiling. A run at one constant load cannot show this, so step the load. In LoadFocus, the &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/what-is-ramp-up-steps-in-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=amdahls-law&quot;&gt;Ramp Up Steps&lt;/a&gt; setting adds virtual users in equal batches, and the &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/reading-the-capacity-panel?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=amdahls-law&quot;&gt;Capacity panel&lt;/a&gt; groups a finished run by active virtual users and, on paid plans, lists p95, requests per second and error rate per level for single-generator runs. The panel names a capacity level only when the run climbed through at least three levels and the top level was at least double the lowest. It fits no scalability model, so the fit below is yours to do.&lt;/p&gt;
&lt;h2&gt;What is Gustafson&amp;#39;s law?&lt;/h2&gt;
&lt;p&gt;It is the counterpoint: if the problem grows with the machine, the serial share stays small and speedup grows almost linearly. Amdahl holds the work fixed; Gustafson holds the run time fixed.&lt;/p&gt;
&lt;p&gt;John Gustafson&amp;#39;s 1988 note &lt;a href=&quot;https://doi.org/10.1145/42411.42415&quot;&gt;Reevaluating Amdahl&amp;#39;s law&lt;/a&gt; reported speedups of 1016 to 1021 on a 1024-processor hypercube at Sandia, for applications with a serial share of 0.4 to 0.8 percent, and argued that users enlarge the problem to fill the time they have. With s&amp;#39; as the serial share measured on the parallel run, scaled speedup, a formula the note credits to E. Barsis at Sandia, is N + (1 − N)s&amp;#39;.&lt;/p&gt;
&lt;p&gt;At s&amp;#39; = 0.05 and 64 processors that gives 60.85, against 15.42 for Amdahl, because the two 5% figures come from different machines. In a web system, Gustafson fits when each added server works on its own share of the data; Amdahl fits when every extra user still touches the same shared row.&lt;/p&gt;
&lt;h2&gt;What does the Universal Scalability Law add?&lt;/h2&gt;
&lt;p&gt;A second term for coherency, the cost of keeping shared data consistent between workers, which grows with the number of worker pairs. With it, throughput can peak and then fall as you add capacity.&lt;/p&gt;
&lt;p&gt;Gunther&amp;#39;s &lt;a href=&quot;https://arxiv.org/abs/0808.1431&quot;&gt;A General Theory of Computational Scalability Based on Rational Functions&lt;/a&gt; (arXiv, 2008) states the Universal Scalability Law (USL) as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C(N) = N / (1 + σ(N - 1) + κN(N - 1))
peak at N* = sqrt((1 - σ) / κ)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;σ is contention (serialization or queueing) and κ is coherency (inconsistent copies of data); with κ = 0 it is Amdahl&amp;#39;s formula. Gunther&amp;#39;s worst-case examples include updating database records, and he calls throughput beyond N* &amp;quot;retrograde&amp;quot;. With illustrative values σ = 0.05 and κ = 0.001, the peak sits near 31 workers: relative throughput is 9.03 at 32 and 7.82 at 64, where the σ term alone predicts 15.42.&lt;/p&gt;
&lt;h2&gt;How do you fit the USL to a load test series?&lt;/h2&gt;
&lt;p&gt;Measure throughput at several concurrency levels, divide each by the throughput at one user, and fit σ and κ by least squares.&lt;/p&gt;
&lt;p&gt;Rearranged, the formula reads N / C − 1 = σ(N − 1) + κN(N − 1), which is linear in σ and κ. The values below are generated from σ = 0.05 and κ = 0.001; put your own requests per second per level in x:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np

n = np.array([1, 2, 4, 8, 16, 32])          # virtual users
x = np.array([50, 95, 172, 284, 402, 452])  # req/s per level
c = x / x[0]                                # relative capacity
A = np.column_stack([n - 1, n * (n - 1)])
(s, k), *_ = np.linalg.lstsq(A, n / c - 1, rcond=None)
print(f&amp;quot;sigma={s:.3f} kappa={k:.4f}&amp;quot;)
print(f&amp;quot;peak near {np.sqrt((1 - s) / k):.0f} users&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It prints &lt;code&gt;sigma=0.050&lt;/code&gt;, &lt;code&gt;kappa=0.0010&lt;/code&gt; and a peak near 31 users. Change only concurrency between levels, and include a one-user level, since every point is divided by it.&lt;/p&gt;
&lt;h2&gt;Where do these models stop being useful?&lt;/h2&gt;
&lt;p&gt;They compress a throughput curve into one or two constants, so they cannot name the lock at fault or follow a system that changes under load. Use them to read a measured curve and extend it a short way: a fit on levels far below the knee says little about the peak.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Constant parameters. A warming cache, an autoscaler or a pool hitting its limit changes σ and κ mid-test.&lt;/li&gt;
&lt;li&gt;Throughput only. p95 and p99 can degrade first: check &lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;tail latency&lt;/a&gt; too.&lt;/li&gt;
&lt;li&gt;The load generator. If it saturates first, the flat curve is its own.&lt;/li&gt;
&lt;li&gt;Closed tests. Fixed virtual users measure a &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;closed system&lt;/a&gt;; real users arrive at their own rate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Can you explain Amdahl&amp;#39;s law in a simple way?&lt;/strong&gt;
A job is only as fast as the part of it that cannot be shared out. If one tenth of the work must run serially, no number of extra workers makes the job more than ten times faster, because that tenth still takes the same time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is Gustafson&amp;#39;s law?&lt;/strong&gt;
Gustafson&amp;#39;s law says that when the problem grows with the number of processors, speedup grows almost linearly, as N + (1 − N)s&amp;#39;, with s&amp;#39; the serial share of the parallel run. John Gustafson published it in 1988 (crediting the formula to his Sandia colleague E. Barsis) as a counterpoint to Amdahl&amp;#39;s fixed-size assumption.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/queueing-theory/&quot;&gt;Queueing theory: what it is and why latency spikes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Queueing theory: what it is and why latency spikes</title><link>https://loadtest.app/queueing-theory/</link><guid isPermaLink="true">https://loadtest.app/queueing-theory/</guid><description>Queueing theory explained: arrivals, servers and Kendall&apos;s notation, why response time climbs sharply near full utilization, and what that means for tests.</description><pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Queueing theory is the mathematics of waiting lines: from how fast work arrives, how long each job takes and how many servers there are, it predicts how long jobs wait and how long the queue gets. For software, its most useful result is that response time grows slowly at moderate load and then sharply as a server approaches 100% busy. In the simplest model, a server busy 90% of the time answers ten times slower on average than an idle one.&lt;/p&gt;
&lt;p&gt;The figures below are arithmetic from standard formulas, checked with a script, with no test run behind them. Sources were checked on 1 November 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Utilization ρ&lt;/th&gt;
&lt;th&gt;Time in system W ÷ service time S&lt;/th&gt;
&lt;th&gt;If S = 20 ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;3.3&lt;/td&gt;
&lt;td&gt;67 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;100 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;400 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;2,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;These are means for the M/M/1 queue: one server, Poisson arrivals and exponential service times.&lt;/p&gt;
&lt;h2&gt;What is queueing theory?&lt;/h2&gt;
&lt;p&gt;It is the branch of probability that models work arriving, waiting and being served. A queue is described by its arrivals, its service times, its number of servers and its queue discipline, the order in which waiting jobs are picked.&lt;/p&gt;
&lt;p&gt;Larson and Odoni&amp;#39;s &lt;a href=&quot;https://web.mit.edu/urban_or_book/www/book/chapter4/4.2.html&quot;&gt;Urban Operations Research&lt;/a&gt;, an open MIT text, lists bank tellers, checkout counters and toll booths as classic cases. A web server fits the same frame: requests arrive, threads or cores serve them, and the rest wait in a queue.&lt;/p&gt;
&lt;h2&gt;Where did queueing theory come from?&lt;/h2&gt;
&lt;p&gt;From the telephone exchange, where A. K. Erlang worked out the first results for the Copenhagen Telephone Company.&lt;/p&gt;
&lt;p&gt;Erlang&amp;#39;s 1909 paper showed that calls made at random follow the Poisson distribution, and his 1917 formula for loss and waiting time was soon used by telephone companies in many countries, according to his &lt;a href=&quot;https://mathshistory.st-andrews.ac.uk/Biographies/Erlang/&quot;&gt;MacTutor biography&lt;/a&gt;. David Kendall&amp;#39;s 1953 paper, &lt;a href=&quot;https://doi.org/10.1214/aoms/1177728975&quot;&gt;Stochastic Processes Occurring in the Theory of Queues and their Analysis by the Method of the Imbedded Markov Chain&lt;/a&gt;, introduced the notation still in use, as Geoffrey Grimmett&amp;#39;s &lt;a href=&quot;https://arxiv.org/abs/0810.1091&quot;&gt;account of his work&lt;/a&gt; notes. Little&amp;#39;s &lt;a href=&quot;https://doi.org/10.1287/opre.9.3.383&quot;&gt;proof of L = λW&lt;/a&gt; and Kingman&amp;#39;s heavy-traffic results followed in 1961.&lt;/p&gt;
&lt;h2&gt;What does Kendall&amp;#39;s notation such as M/M/1 mean?&lt;/h2&gt;
&lt;p&gt;It names a queue in three parts, A/S/c: the distribution of gaps between arrivals, that of service times, and the number of servers. M/M/1 means random arrivals, exponential service times and one server.&lt;/p&gt;
&lt;p&gt;M stands for memoryless (Poisson arrivals or exponential times), D for deterministic and G for general, meaning any distribution: M/D/1 is one server with a constant service time.&lt;/p&gt;
&lt;h2&gt;How does response time grow with utilization?&lt;/h2&gt;
&lt;p&gt;In an M/M/1 queue, the mean time in the system is W = 1/(μ − λ), which equals S/(1 − ρ). As utilization ρ approaches 1, W grows without limit.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lambda = arrival rate        mu = service rate (1 / S)
rho    = lambda / mu         (utilization, must stay below 1)
W      = 1 / (mu - lambda)   = S / (1 - rho)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Larson and Odoni derive this for a &lt;a href=&quot;https://web.mit.edu/urban_or_book/www/book/chapter4/4.6.1.html&quot;&gt;single-operator call centre&lt;/a&gt; and show there is no steady state at ρ ≥ 1: the queue keeps growing. The same source shows the time in the system is exponentially distributed, so every percentile scales by the same 1/(1 − ρ) factor, and the p95 is about three times the mean: 600 ms in the 0.9 row above.&lt;/p&gt;
&lt;h2&gt;Why does latency knee before the CPU reaches 100%?&lt;/h2&gt;
&lt;p&gt;Because a request waits whenever it finds the server busy, and that happens more often as utilization rises. Under M/M/1 assumptions, response time has already doubled at 50% utilization.&lt;/p&gt;
&lt;p&gt;The saturating resource may also be a connection pool, a database lock or a single-threaded event loop, and the busiest one sets the curve. The early knee is why target utilization sits well below 100%. Work back from your latency budget: with S = 20 ms and a 100 ms mean target, the table allows about 80%, and a 60 ms target about 67%, before bursts take their share.&lt;/p&gt;
&lt;p&gt;LoadFocus&amp;#39;s &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/reading-the-capacity-panel?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=queueing-theory&quot;&gt;Capacity panel&lt;/a&gt; groups a finished run by the number of active virtual users and reports the last level that held before the error rate passed 1% or the p95 rose above double the best level, provided no higher level recovered. It needs a single-generator run that ramps up through at least three load levels, with the top at least double the lowest. By the arithmetic above, if the lightest level left the bottleneck nearly idle, the doubling trips just past 50% utilization; if the lightest level already loaded it to ρ0, the doubling trips later, above (1 + ρ0)/2 under M/M/1.&lt;/p&gt;
&lt;h2&gt;Why does variability matter as much as utilization?&lt;/h2&gt;
&lt;p&gt;Queues come from randomness: bursts of arrivals and occasional slow jobs. In heavy traffic, waiting time grows with the variability of both, multiplied by a ρ/(1 − ρ) term.&lt;/p&gt;
&lt;p&gt;Kingman&amp;#39;s 1961 paper, &lt;a href=&quot;https://doi.org/10.1017/S0305004100036094&quot;&gt;The single server queue in heavy traffic&lt;/a&gt;, shows that when a single server&amp;#39;s traffic intensity is near 1, the queue is long and the waiting time approximately exponential. Course notes from &lt;a href=&quot;https://www.win.tue.nl/~iadan/2V050/h10.pdf&quot;&gt;TU Eindhoven&lt;/a&gt; and the &lt;a href=&quot;https://serveng.technion.ac.il/files/Lectures/Lecture_GGQ&apos;s_FULL_Marked.pdf&quot;&gt;Technion&lt;/a&gt; give an approximation for its mean (the Technion calls it the Allen-Cunneen approximation; it is also widely known as Kingman&amp;#39;s formula):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Wq ≈ rho / (1 - rho) × (ca² + cs²) / 2 × S
ca, cs = coefficient of variation (std dev / mean)
         of arrival gaps and of service times
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wq is the wait before service starts. Exponential times have c² = 1, giving the M/M/1 wait of 9S at 90% utilization. A constant service time halves it to 4.5S; cs² = 4, as a mix of cache hits and slow database calls can produce, raises it to 22.5S. Steady synthetic traffic is less variable than production, so a test tends to show less queueing than real bursts will.&lt;/p&gt;
&lt;h2&gt;How do more servers (M/M/c) help?&lt;/h2&gt;
&lt;p&gt;Several servers sharing one queue absorb random bursts much better than the same servers with a queue each, so each server can run busier for the same wait.&lt;/p&gt;
&lt;p&gt;From Larson and Odoni&amp;#39;s &lt;a href=&quot;https://web.mit.edu/urban_or_book/www/book/chapter4/4.6.2.html&quot;&gt;M/M/m formulas&lt;/a&gt;, with every server 90% busy, W is 10S for one server, 5.3S for two sharing a queue and 3.0S for four. Four instances behind a balancer that picks a free one behave like the last case; four that each take a random quarter of the traffic behave like separate M/M/1 queues at 10S.&lt;/p&gt;
&lt;h2&gt;How does Little&amp;#39;s law fit in?&lt;/h2&gt;
&lt;p&gt;Little&amp;#39;s law, L = λW, ties the average number of requests in a system to their arrival rate and their time inside, whatever the distributions. Queueing theory gives you W, and Little&amp;#39;s law turns it into L.&lt;/p&gt;
&lt;p&gt;In the 0.9 row, 45 requests per second at W = 200 ms means 9 requests inside on average. Our &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law article&lt;/a&gt; uses the same law to size and check load tests.&lt;/p&gt;
&lt;h2&gt;Where do these models break down in load testing?&lt;/h2&gt;
&lt;p&gt;They assume an open stream of independent arrivals and a steady state. A test with a fixed number of virtual users is a closed system, where arrivals slow down as responses do.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Closed tests. With N virtual users, at most N requests are in the system, so the request rate falls where an open queue would grow, and &lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;think time&lt;/a&gt; lowers it further. Our &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; article explains why that hides saturation.&lt;/li&gt;
&lt;li&gt;LoadFocus runs &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=queueing-theory&quot;&gt;k6 scripts&lt;/a&gt;, but arrival-rate scenarios run there with a fixed number of users, as its script validator warns. Read those runs as closed tests: past the knee, expect a flat request rate and rising response times.&lt;/li&gt;
&lt;li&gt;Steady state. Warm-up, caches and autoscaling change the service rate mid-run, so compare levels held long enough to settle.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is it queueing theory or queuing theory?&lt;/strong&gt;
Both spellings are correct and name the same field. Kingman&amp;#39;s 1961 paper writes &amp;quot;queueing time&amp;quot;, while Little&amp;#39;s proof from the same year has &amp;quot;Queuing Formula&amp;quot; in its title.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does queueing theory need Poisson arrivals?&lt;/strong&gt;
Only for exact M/M/1 results. G/G/1 approximations such as Kingman&amp;#39;s formula use the mean and variability of arrival gaps and service times, and Little&amp;#39;s law needs no assumption about the distributions.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;Think time in load testing: what it is and how to set it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SLO vs SLA: the difference, SLIs and error budgets</title><link>https://loadtest.app/slo-vs-sla/</link><guid isPermaLink="true">https://loadtest.app/slo-vs-sla/</guid><description>SLO vs SLA explained: who sets each, what an SLI is, how error budgets work, the downtime 99.9% allows, and how to check an SLO with a load test.</description><pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;An SLA (service level agreement) is a contract with a customer that says what happens when a service misses a promised level, usually a service credit. An SLO (service level objective) is the internal target a team runs the service against; the SRE book advises setting it stricter than the SLA so that problems get fixed before the contract is breached. An SLI (service level indicator) is the measurement both are written in, such as the share of requests that succeed.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SLI&lt;/th&gt;
&lt;th&gt;SLO&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;A measurement&lt;/td&gt;
&lt;td&gt;A target for an SLI&lt;/td&gt;
&lt;td&gt;A contract built on SLOs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set by&lt;/td&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;Product owner and engineering&lt;/td&gt;
&lt;td&gt;Business and legal teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On a miss&lt;/td&gt;
&lt;td&gt;Nothing by itself&lt;/td&gt;
&lt;td&gt;Error budget spent&lt;/td&gt;
&lt;td&gt;Service credits or penalties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example&lt;/td&gt;
&lt;td&gt;Checkout calls that succeed&lt;/td&gt;
&lt;td&gt;99.95% over four weeks&lt;/td&gt;
&lt;td&gt;99.9% monthly, or a credit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The examples are illustrations; the sources are Google&amp;#39;s SRE book and workbook and the AWS compute SLA, checked on 1 November 2026.&lt;/p&gt;
&lt;h2&gt;What do SLI, SLO and SLA stand for?&lt;/h2&gt;
&lt;p&gt;Service level indicator, service level objective and service level agreement. The indicator is what you measure, the objective is the value you aim for, and the agreement is what you promise someone else, with consequences attached.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://sre.google/sre-book/service-level-objectives/&quot;&gt;SRE book chapter on Service Level Objectives&lt;/a&gt; defines an SLA as &amp;quot;an explicit or implicit contract with your users that includes consequences of meeting (or missing) the SLOs they contain&amp;quot;. The chapter&amp;#39;s test is one question: what happens if the SLOs are not met? With no explicit consequence, you are almost certainly looking at an SLO.&lt;/p&gt;
&lt;p&gt;The chapter adds that most people mean SLO when they say SLA, and that a real SLA violation might end in a breach-of-contract case. So when a meeting turns into an SLA vs SLO argument, first ask whether a consequence, usually money, is written into a contract.&lt;/p&gt;
&lt;p&gt;The SRE book advises keeping a safety margin: &amp;quot;a tighter internal SLO than the SLO advertised to users gives you room to respond to chronic problems before they become visible externally.&amp;quot; Its &amp;quot;Embracing Risk&amp;quot; chapter applies the same idea to a contract: a 99.9% external target backed by a stronger internal one.&lt;/p&gt;
&lt;h2&gt;What is an error budget?&lt;/h2&gt;
&lt;p&gt;It is 100% minus the SLO: the share of requests or time that may fail within the window. While budget remains, the team keeps shipping; once it is spent, reliability work comes first.&lt;/p&gt;
&lt;p&gt;The SRE workbook chapter &lt;a href=&quot;https://sre.google/workbook/implementing-slos/&quot;&gt;&amp;quot;Implementing SLOs&amp;quot;&lt;/a&gt; gives the arithmetic: with a 99.9% SLO, a service that receives 3 million requests over four weeks may fail 3,000 of them, so one outage with 1,500 errors spends half the budget. The book&amp;#39;s &lt;a href=&quot;https://sre.google/sre-book/embracing-risk/&quot;&gt;&amp;quot;Embracing Risk&amp;quot;&lt;/a&gt; chapter explains the purpose: product management sets the SLO, monitoring measures uptime, and the gap is the unreliability left for the quarter. When that budget runs out, releases are halted or slowed.&lt;/p&gt;
&lt;h2&gt;How much downtime does 99.9% allow?&lt;/h2&gt;
&lt;p&gt;About 43 minutes in a 30-day window. Each extra nine divides the allowance by ten, so 99.99% leaves just over 4 minutes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLO&lt;/th&gt;
&lt;th&gt;Error budget&lt;/th&gt;
&lt;th&gt;Downtime per 30 days&lt;/th&gt;
&lt;th&gt;Failures per million&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;7 h 12 min&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;td&gt;3 h 36 min&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;43 min 12 s&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.95%&lt;/td&gt;
&lt;td&gt;0.05%&lt;/td&gt;
&lt;td&gt;21 min 36 s&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.99%&lt;/td&gt;
&lt;td&gt;0.01%&lt;/td&gt;
&lt;td&gt;4 min 19 s&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Multiply the 43,200 minutes of a 30-day window by the error budget. Note the workbook&amp;#39;s warning that each extra nine costs more while its value to users approaches zero.&lt;/p&gt;
&lt;h2&gt;What does a real SLA look like?&lt;/h2&gt;
&lt;p&gt;It names a measured percentage, a billing period, a credit schedule and what does not count. The AWS compute SLA is a short public example.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://aws.amazon.com/compute/sla/&quot;&gt;Amazon Compute Service Level Agreement&lt;/a&gt; (last updated 25 May 2022; checked on 1 November 2026) commits to a Monthly Uptime Percentage of at least 99.99% per region for EC2 instances spread over two or more Availability Zones, and 99.5% for a single instance. Uptime is 100% minus the share of minutes when EC2 was unavailable. Under the Region-Level SLA, below 99.99% the credit is 10% of that month&amp;#39;s EC2 bill in the region, below 99.0% it is 30%, and below 95.0% it is 100%; a single instance&amp;#39;s 10% tier starts below 99.5%. Credits are applied against future payments, must be claimed through AWS Support and are the &amp;quot;sole and exclusive&amp;quot; remedy unless the customer agreement says otherwise.&lt;/p&gt;
&lt;p&gt;Read against the downtime table, the Region-Level SLA&amp;#39;s 10% tier spans roughly 4 minutes to 7 hours of unavailability in a 30-day month. An SLA caps what the provider owes; keeping outages from hurting your users is the job of your own objectives.&lt;/p&gt;
&lt;h2&gt;How do you choose SLIs for latency and availability?&lt;/h2&gt;
&lt;p&gt;Measure latency as a percentile and availability as good events divided by all events, as close to the user as you can. Averages and server-only numbers hide the requests people complain about.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; The SRE book prefers percentiles to the mean, because an average can stay flat while a long tail gets much slower. The workbook writes latency SLIs as a ratio, such as calls completed in under 100 ms divided by all calls. So &amp;quot;99% of checkout calls under 400 ms&amp;quot; means &amp;quot;p99 of checkout under 400 ms&amp;quot; over the window, which carries straight into a test. Our &lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;tail latency&lt;/a&gt; article explains why p99 matters once a page calls several services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Availability.&lt;/strong&gt; Time-based availability counts minutes of uptime, as the AWS SLA does. Request-based availability counts successful requests: &amp;quot;Embracing Risk&amp;quot; describes a service with 2.5 million requests a day and a 99.99% daily target, which can return 250 errors and still meet it.&lt;/p&gt;
&lt;h2&gt;Where do SLOs go wrong?&lt;/h2&gt;
&lt;p&gt;Usually through too many objectives, 100% targets and indicators measured where users are not. Each one leaves a number nobody acts on, or one that looks fine while users suffer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Too many SLOs.&lt;/strong&gt; The SRE book advises as few as possible: if quoting an SLO can never win a conversation about priorities, it is probably not worth having.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;100% targets.&lt;/strong&gt; The workbook calls 100% the wrong target: components fail, change is the main source of outages, and a 100% SLO leaves time only to react.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measuring at the server only.&lt;/strong&gt; Server metrics miss problems users feel, such as a slow page script. The workbook suggests measuring at the load balancer or the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What does load testing have to do with SLOs?&lt;/h2&gt;
&lt;p&gt;An SLO gives a load test its pass/fail criteria, and a test at the expected peak shows whether the latency SLO holds before that traffic arrives. Production monitoring only tells you afterwards.&lt;/p&gt;
&lt;p&gt;The SRE book notes that higher QPS often leads to larger latencies and that services commonly have a performance cliff beyond some load threshold. A load test can find that cliff if it runs at the expected peak with a realistic request mix and measures the SLI the SLO uses. Set each test limit a little tighter than the SLO, as our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; describes. A run lasts minutes, so it cannot measure monthly availability.&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, grades load tests against &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/pass-fail-thresholds?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=slo-vs-sla&quot;&gt;pass/fail thresholds&lt;/a&gt; saved on the test: Max P95 and Max P99 response time, a maximum error rate and a minimum throughput, for cloud, JMeter and k6 tests. Its API monitoring has &lt;a href=&quot;https://loadfocus.com/docs/guides/api-monitoring/sla-slo-reports?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=slo-vs-sla&quot;&gt;SLA / SLO reports&lt;/a&gt; that set an SLO target per monitor and show uptime, error budget consumed and an OK or Breached status.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What does SLO stand for?&lt;/strong&gt;
Service level objective: a target value for a measured service level, such as 99.9% of requests succeeding over four weeks. It is set internally, by product and engineering, and carries no contractual penalty by itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can you have an SLO without an SLA?&lt;/strong&gt;
Yes. The SRE book gives Google Search as an important service with no public SLA that is still managed with SLIs and SLOs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should an SLO ever be 100%?&lt;/strong&gt;
No. A 100% objective leaves no error budget, so every failure breaches it and no change is ever safe to release.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score: what it is and how to calculate it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>TPC-H: what the benchmark measures and how to run it</title><link>https://loadtest.app/tpc-h/</link><guid isPermaLink="true">https://loadtest.app/tpc-h/</guid><description>TPC-H explained: 8 tables, 22 queries, scale factors, QphH and the power and throughput tests, audited vs informal runs, and when TPC-DS fits better.</description><pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;TPC-H is the decision support benchmark of the Transaction Processing Performance Council (TPC): 22 business queries and two refresh functions run against eight tables that the TPC&amp;#39;s dbgen program fills at a chosen scale factor, where scale factor 1 is about 1 GB. Its metric, QphH@Size, combines a single-stream power test with a multi-stream throughput test, and official results add a price per kQphH@Size. Only audited results published on tpc.org count as official; any TPC-H-style number published outside tpc.org counts as a derived workload under the TPC&amp;#39;s fair use rules and may not be compared with official results.&lt;/p&gt;
&lt;p&gt;We have not run the benchmark for this guide. The facts come from tpc.org, the specification revision 3.0.1 that tpc.org lists as current, and each tool&amp;#39;s documentation, all checked on 1 November 2026. The judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;TPC-H&lt;/th&gt;
&lt;th&gt;TPC-C&lt;/th&gt;
&lt;th&gt;TPC-DS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Workload&lt;/td&gt;
&lt;td&gt;Decision support: ad hoc queries plus data refreshes&lt;/td&gt;
&lt;td&gt;OLTP: a wholesale supplier&amp;#39;s orders&lt;/td&gt;
&lt;td&gt;Decision support: queries plus data maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;8 tables, 22 queries, 2 refresh functions&lt;/td&gt;
&lt;td&gt;9 table types, 5 transaction types&lt;/td&gt;
&lt;td&gt;Snowflake schema, 7 fact tables, 99 queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary metric&lt;/td&gt;
&lt;td&gt;QphH@Size and $/kQphH@Size&lt;/td&gt;
&lt;td&gt;tpmC&lt;/td&gt;
&lt;td&gt;QphDS@SF and $/kQphDS@SF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current version&lt;/td&gt;
&lt;td&gt;3.0.1&lt;/td&gt;
&lt;td&gt;5.11.0&lt;/td&gt;
&lt;td&gt;4.0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What does TPC-H measure?&lt;/h2&gt;
&lt;p&gt;How fast a system answers complex analytical queries over a large dataset while new sales arrive and old ones are deleted. One specification defines the schema, the queries, the data and the rules.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.tpc.org/tpch/&quot;&gt;TPC-H page&lt;/a&gt; describes &amp;quot;a suite of business oriented ad-hoc queries and concurrent data modifications&amp;quot; (checked on 1 November 2026). The &lt;a href=&quot;https://www.tpc.org/TPC_Documents_Current_Versions/pdf/TPC-H_v3.0.1.pdf&quot;&gt;specification&lt;/a&gt; defines eight tables: PART, PARTSUPP, LINEITEM, ORDERS, CUSTOMER, SUPPLIER, NATION and REGION. All but the last two grow with the scale factor; LINEITEM, the largest, holds about 6,000,000 rows per unit. The refresh function RF1 inserts new rows into ORDERS and LINEITEM, and RF2 removes old sales (checked on 1 November 2026).&lt;/p&gt;
&lt;h2&gt;How is the TPC-H score calculated?&lt;/h2&gt;
&lt;p&gt;A run is a power test followed by a throughput test, and QphH@Size is the geometric mean of the two results. The price metric is the total system price divided by QphH@Size, times 1,000.&lt;/p&gt;
&lt;p&gt;In the power test, a refresh stream runs RF1, a single query session runs all 22 queries once, then the refresh stream runs RF2. Power@Size is 3,600 times the scale factor divided by the geometric mean of those 24 timings. In the throughput test, several query streams run beside a refresh stream: at least 2 at scale factor 1, rising to 11 at scale factor 100,000. Throughput@Size is queries per hour across all streams, times the scale factor. A full test has two runs, and the sponsor reports the one with the lower QphH (all from the &lt;a href=&quot;https://www.tpc.org/TPC_Documents_Current_Versions/pdf/TPC-H_v3.0.1.pdf&quot;&gt;specification&lt;/a&gt;, checked on 1 November 2026).&lt;/p&gt;
&lt;p&gt;The geometric mean weighs every query equally, so halving one query&amp;#39;s time raises Power by only about 3%. An official result uses a scale factor from a fixed list: 1, 10, 30, 100 and so on up to 100,000.&lt;/p&gt;
&lt;h2&gt;Who publishes TPC-H results, and which ones count?&lt;/h2&gt;
&lt;p&gt;Vendors that sponsor a test publish audited results on tpc.org, and only those count as official. Any other run is a derived workload with its own rules.&lt;/p&gt;
&lt;p&gt;The specification requires an independent audit by a TPC-certified auditor. The TPC&amp;#39;s &lt;a href=&quot;https://www.tpc.org/TPC_Documents_Current_Versions/pdf/Fair_Use_Quick_Reference_v1.0.0.pdf&quot;&gt;fair use quick reference&lt;/a&gt; allows the TPC name next to results only for official published results. Anything else needs the prefix &amp;quot;Derived from&amp;quot;, may not use the TPC metric, must carry a disclaimer that it is not comparable to published results, should disclose its environment and deviations, and may not be compared with official results (checked on 1 November 2026).&lt;/p&gt;
&lt;h2&gt;How do you run TPC-H yourself?&lt;/h2&gt;
&lt;p&gt;Generate the data and query text, load the tables, then run the power test followed by the throughput test. You can use the TPC&amp;#39;s own tools, a kit built for one database or an engine with a built-in generator.&lt;/p&gt;
&lt;p&gt;The TPC&amp;#39;s &lt;a href=&quot;https://www.tpc.org/tpc_documents_current_versions/current_specifications5.asp&quot;&gt;current specifications page&lt;/a&gt; offers a free tools package: you register, accept a licence and get the link by email. The specification says DBGen must produce the data and QGen the query text (checked on 1 November 2026).&lt;/p&gt;
&lt;p&gt;For PostgreSQL, the &lt;a href=&quot;https://wiki.postgresql.org/wiki/TPC-H&quot;&gt;wiki&amp;#39;s TPC-H page&lt;/a&gt; gives no step-by-step procedure and points to &lt;a href=&quot;https://wiki.postgresql.org/wiki/DBT-3&quot;&gt;DBT-3&lt;/a&gt;, a full kit that the wiki&amp;#39;s DBT-3 page calls a fair usage implementation of the specification. &lt;strong&gt;HammerDB&lt;/strong&gt; ships TPROC-H, which its &lt;a href=&quot;https://www.hammerdb.com/docs/ch11s01.html&quot;&gt;documentation&lt;/a&gt; calls an open source workload derived from TPC-H and not comparable to published results. It runs on PostgreSQL, MySQL, Oracle and other engines, and the docs recommend comparing the &lt;a href=&quot;https://www.hammerdb.com/docs/ch11s02.html&quot;&gt;geometric mean of query times&lt;/a&gt; instead of calculating QphH (all checked on 1 November 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DuckDB&lt;/strong&gt; has a &lt;code&gt;tpch&lt;/code&gt; extension that, per its &lt;a href=&quot;https://duckdb.org/docs/stable/core_extensions/tpch&quot;&gt;documentation&lt;/a&gt;, implements the data generator and the queries. These lines generate scale factor 1, run query 4 and list the 22 queries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CALL dbgen(sf = 1);
PRAGMA tpch(4);
FROM tpch_queries();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same page warns that the data differs slightly from the specification, that each query runs with fixed parameters and that &lt;code&gt;dbgen&lt;/code&gt; does not drop existing tables. For fully compliant data it points to &lt;code&gt;tpchgen-cli&lt;/code&gt; (checked on 1 November 2026).&lt;/p&gt;
&lt;h2&gt;How do you read TPC-H results honestly?&lt;/h2&gt;
&lt;p&gt;Compare results only at the same scale factor and benchmark version, and read each metric with its size suffix. A chart of single query times shows neither the power test nor the throughput test.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.tpc.org/tpch/results/tpch_results5.asp&quot;&gt;results page&lt;/a&gt; groups results by database size and carries three notes: version 2 and version 3 results are not comparable, price/performance in different currencies should not be compared, and comparisons across database sizes are misleading (checked on 1 November 2026). The specification adds that results with different stream counts are comparable at the same database size.&lt;/p&gt;
&lt;p&gt;For an informal run, look for what the fair use rules ask for: hardware and memory, changes to schema or query text, and whether refresh functions ran. A post that times the 22 queries once on a warm cache, with no refresh stream, approximates only the query half of a power test, though such a run still helps with regression tracking. On query rate against response time, see &lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;throughput vs latency&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;When should you use something other than TPC-H?&lt;/h2&gt;
&lt;p&gt;When the workload is transactional, when you want a newer analytics benchmark, or when the question is what users feel. The benchmark models one analytical workload, without short transactions or an application layer.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;TPC-DS&lt;/strong&gt; for a broader decision support test: its &lt;a href=&quot;https://www.tpc.org/TPC_Documents_Current_Versions/pdf/TPC-DS_v4.0.0.pdf&quot;&gt;version 4.0.0 specification&lt;/a&gt; uses a snowflake schema with seven fact tables and 99 queries, and its &lt;a href=&quot;https://www.tpc.org/tpcds/&quot;&gt;overview page&lt;/a&gt; says version 4 results are not comparable with earlier versions. Choose &lt;strong&gt;TPC-C&lt;/strong&gt;, or HammerDB&amp;#39;s TPROC-C, for order-entry OLTP: the &lt;a href=&quot;https://www.tpc.org/tpcc/&quot;&gt;TPC-C page&lt;/a&gt; describes five transaction types measured in tpmC (all checked on 1 November 2026). For a quick check of one database, &lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench&lt;/a&gt; covers PostgreSQL and &lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;sysbench&lt;/a&gt; covers MySQL.&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run TPC-H; its tests are aimed at web and API endpoints. It &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tpc-h&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions, with no JDBC driver and no SQL module, so it cannot connect to the database. Choose a database benchmark tool such as DuckDB&amp;#39;s extension, DBT-3 or HammerDB instead of LoadFocus for this job, and use LoadFocus for the web or API layer in front of the database, starting from its &lt;a href=&quot;https://loadfocus.com/templates/load-testing-for-database-query-performance?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tpc-h&quot;&gt;template for the endpoints behind heavy queries&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between TPC-H and TPC-DS?&lt;/strong&gt;
Both are TPC decision support benchmarks. TPC-H uses 8 tables and 22 queries and reports QphH@Size, while TPC-DS uses a snowflake schema with seven fact tables and 99 queries and reports QphDS@SF.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What does QphH@Size mean in TPC-H?&lt;/strong&gt;
It is the composite queries-per-hour metric: the geometric mean of the power test and throughput test results at one database size. The suffix names that size, for example QphH@1000GB, and results at different sizes should not be compared.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run TPC-H?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts aimed at web and API endpoints and has no SQL connection, so use a database benchmark tool for TPC-H and LoadFocus for the application layer in front of the database.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench: how to benchmark and load test PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;How to use sysbench for CPU, disk and MySQL benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/ycsb/&quot;&gt;YCSB: workloads A-F, how to run it and its limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/redis-benchmark/&quot;&gt;redis-benchmark: how to use it and read its numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>redis-benchmark: how to use it and read its numbers</title><link>https://loadtest.app/redis-benchmark/</link><guid isPermaLink="true">https://loadtest.app/redis-benchmark/</guid><description>How to use redis-benchmark: the key options, how to read its throughput and latency output, what pipelining and clients change, and why your app differs.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;redis-benchmark is the benchmark utility that ships with Redis: it opens a number of client connections (50 by default), sends a fixed number of commands (100,000 by default) and reports requests per second and latency for each test. Run &lt;code&gt;redis-benchmark -q -n 100000&lt;/code&gt; against a running server, narrow it to the commands you care about with &lt;code&gt;-t&lt;/code&gt;, and read the result as the speed of one Redis instance under synthetic commands. Your application adds its own network path, client library and command mix, so expect its figures to differ.&lt;/p&gt;
&lt;p&gt;We ran it on 31 October 2026 on an Apple M1 Max laptop, Redis 7.2.6 server and client on one machine over loopback, persistence off. Each row is one run of 100,000 requests, with requests per second rounded to the nearest thousand; p50 is as printed.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Requests per second&lt;/th&gt;
&lt;th&gt;p50 (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t set,get&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing: 50 clients, 3-byte values&lt;/td&gt;
&lt;td&gt;SET 113,000, GET 118,000&lt;/td&gt;
&lt;td&gt;0.367, 0.351&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t set,get -P 16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16 commands per round trip&lt;/td&gt;
&lt;td&gt;SET 1,010,000, GET 1,190,000&lt;/td&gt;
&lt;td&gt;0.711, 0.583&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t set,get -c 200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200 clients&lt;/td&gt;
&lt;td&gt;SET 117,000, GET 123,000&lt;/td&gt;
&lt;td&gt;1.135, 1.071&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t set -d 1024&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,024-byte values&lt;/td&gt;
&lt;td&gt;SET 114,000&lt;/td&gt;
&lt;td&gt;0.367&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t set -r 100000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Random keys out of 100,000&lt;/td&gt;
&lt;td&gt;SET 116,000&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;script load &amp;quot;...&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A custom command&lt;/td&gt;
&lt;td&gt;106,000&lt;/td&gt;
&lt;td&gt;0.415&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;How do you run redis-benchmark?&lt;/h2&gt;
&lt;p&gt;Point the tool at a running server with &lt;code&gt;-h&lt;/code&gt; and &lt;code&gt;-p&lt;/code&gt;; with no options it runs its default test suite against 127.0.0.1:6379. Add &lt;code&gt;-t&lt;/code&gt; to pick tests and &lt;code&gt;-q&lt;/code&gt; to print one line per test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;redis-benchmark -h 10.0.0.12 -p 6379 -c 50 -n 100000 \
  -t set,get -q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The official &lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/&quot;&gt;Redis benchmark page&lt;/a&gt; and the &lt;code&gt;--help&lt;/code&gt; output of version 7.2.6 list these options, among others (checked on 31 October 2026):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt; parallel connections (default 50) and &lt;code&gt;-n&lt;/code&gt; total requests (default 100,000).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; size of SET and GET values in bytes (default 3).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P&lt;/code&gt; commands per pipeline (default 1, no pipelining).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-t&lt;/code&gt; a comma-separated list of tests; &lt;code&gt;-q&lt;/code&gt; quiet output; &lt;code&gt;--csv&lt;/code&gt; CSV output.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r&lt;/code&gt; random keys from a key space of that size. Without it, every command uses the same key.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-k 1&lt;/code&gt; keeps connections alive (the default), &lt;code&gt;-k 0&lt;/code&gt; reconnects.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--threads&lt;/code&gt; turns on multi-thread mode; &lt;code&gt;--cluster&lt;/code&gt; turns on cluster mode, where &lt;code&gt;-c&lt;/code&gt; must be at least the number of nodes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How do you read redis-benchmark output?&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;-q&lt;/code&gt; you get one line per test: requests per second and the median latency. Without it, each test prints its settings, a latency distribution by percentile and a summary with the average, minimum, p50, p95, p99 and maximum latency.&lt;/p&gt;
&lt;p&gt;The header also prints the server&amp;#39;s &lt;code&gt;save&lt;/code&gt; and &lt;code&gt;appendonly&lt;/code&gt; settings, which tell you whether persistence was part of the test. Our full GET run read 119,617 requests per second, an average of 0.352 ms, p95 0.559 ms, p99 0.623 ms and a maximum of 0.759 ms. A request that makes many Redis calls is likelier to meet a slow one, so read p99 too; see our &lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;tail latency&lt;/a&gt; article.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--csv&lt;/code&gt; prints the summary as one row per test. Repeat runs before you trust a small difference: two runs of the same SET test gave us about 113,000 and 125,000 requests per second.&lt;/p&gt;
&lt;h2&gt;Why does pipelining change the numbers so much?&lt;/h2&gt;
&lt;p&gt;Without &lt;code&gt;-P&lt;/code&gt;, each client waits for a reply before sending its next command, so every command pays a full round trip. With &lt;code&gt;-P 16&lt;/code&gt; each client sends 16 at once, and in our run SET rose from about 113,000 to 1,010,000 requests per second and GET from 118,000 to 1,190,000.&lt;/p&gt;
&lt;p&gt;The official page calls pipelining realistic, since many Redis applications use it, and advises a pipeline size close to the average your application can use. Our median latency rose at the same time, to 0.711 and 0.583 ms.&lt;/p&gt;
&lt;h2&gt;What happens when you add more clients?&lt;/h2&gt;
&lt;p&gt;Once the bottleneck is saturated, more clients only add waiting. On one laptop that can be the server or the single-threaded benchmark client.&lt;/p&gt;
&lt;p&gt;Going from 50 to 200 clients left throughput almost flat (about 117,000 SET and 123,000 GET requests per second) while the median latency roughly tripled, to 1.135 and 1.071 ms. That is &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law&lt;/a&gt; at work. Each client keeps at most one command in flight, so &lt;code&gt;-c&lt;/code&gt; fixes how many are in the system, and if throughput cannot grow, the time each spends there must. In our full GET run, 50 clients ÷ 119,617 requests per second gives 0.42 ms per client loop, a little above the measured average of 0.352 ms. With 200 clients the same calculation gives 1.63 ms.&lt;/p&gt;
&lt;p&gt;This also makes the tool&amp;#39;s load a closed workload: when Redis stalls, the clients wait, so the commands your application would have sent meanwhile never appear. Neither the official page nor &lt;code&gt;--help&lt;/code&gt; lists a fixed-rate option, so read its percentiles with &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt; in mind.&lt;/p&gt;
&lt;h2&gt;How do you benchmark your own command or a Lua script?&lt;/h2&gt;
&lt;p&gt;Put the command after the options. The tool then runs only that command and ignores &lt;code&gt;-t&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;redis-benchmark -n 100000 -q \
  script load &amp;quot;redis.call(&amp;#39;set&amp;#39;,&amp;#39;foo&amp;#39;,&amp;#39;bar&amp;#39;)&amp;quot;
redis-benchmark -r 10000 -n 10000 \
  eval &amp;#39;return redis.call(&amp;quot;ping&amp;quot;)&amp;#39; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line is the official page&amp;#39;s example, which ran at about 106,000 requests per second on our laptop. &lt;code&gt;SCRIPT LOAD&lt;/code&gt; only loads the script, so use &lt;code&gt;eval&lt;/code&gt;, as in the second line from &lt;code&gt;--help&lt;/code&gt;, to time running one. With &lt;code&gt;-r&lt;/code&gt;, the help says &lt;code&gt;__rand_int__&lt;/code&gt; in an argument becomes a random 12-digit number in that range, and in cluster mode the key must contain a &lt;code&gt;{tag}&lt;/code&gt; to reach the right node.&lt;/p&gt;
&lt;h2&gt;Why does your application see different numbers?&lt;/h2&gt;
&lt;p&gt;Your application measures its whole path to Redis: network, client library, serialization and its own command mix. The official page lists pitfalls to check before you compare.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A loop of synchronous commands mostly measures the network and the client library.&lt;/li&gt;
&lt;li&gt;With client and server on one box, the CPU limits the benchmark. Unix domain sockets can reach around 50% more throughput than TCP loopback, on Linux for instance.&lt;/li&gt;
&lt;li&gt;4 KB strings at 100,000 queries per second need about 3.2 Gbit/s, more than a 1 Gbit/s link carries.&lt;/li&gt;
&lt;li&gt;To compare Redis with a transactional database, turn on AOF and choose a suitable fsync policy.&lt;/li&gt;
&lt;li&gt;Laptops vary their CPU frequency, which hurts repeatability.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INFO&lt;/code&gt; at intervals is probably fine during a run, while &lt;code&gt;MONITOR&lt;/code&gt; slows Redis significantly. Our publisher&amp;#39;s roundup of &lt;a href=&quot;https://loadfocus.com/blog/comparisons/redis-monitoring-tools/?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=redis-benchmark&quot;&gt;Redis monitoring tools&lt;/a&gt; covers the metrics worth watching.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Are these laptop numbers Redis&amp;#39;s real capacity?&lt;/h2&gt;
&lt;p&gt;No. They show one laptop talking to itself, with no network, 3-byte values in most runs, no persistence and one run per row.&lt;/p&gt;
&lt;p&gt;The table shows how each option moves the figures. Capacity depends on your server, network, value sizes and persistence settings.&lt;/p&gt;
&lt;h2&gt;When is redis-benchmark the wrong tool?&lt;/h2&gt;
&lt;p&gt;When you want to know what users feel. It sends synthetic commands to Redis alone, while a page&amp;#39;s response time also includes the application code and its other calls.&lt;/p&gt;
&lt;p&gt;For that, load the application or API that uses Redis and watch Redis while the test runs. LoadFocus, which publishes this site, works at that layer: it &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=redis-benchmark&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions against your web app or API. It does not run redis-benchmark.&lt;/p&gt;
&lt;p&gt;For more control over Redis traffic, the &lt;a href=&quot;https://github.com/redis/memtier_benchmark&quot;&gt;memtier_benchmark&lt;/a&gt; repository lists multi-threaded clients, a read:write ratio, random or sequential key patterns, Redis Cluster and TLS (checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is redis-benchmark designed to make Redis look fast?&lt;/strong&gt;
No. The official Redis page calls that a common misconception: by default the tool keeps at most one query pending per connection, with no pipelining, and a client that pipelines can generate more throughput.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do you fill a Redis database with redis-benchmark?&lt;/strong&gt;
Run only the SET test with random keys, as the tool&amp;#39;s help shows: &lt;code&gt;-t set -n 1000000 -r 100000000&lt;/code&gt; fills about one million keys. Random keys can repeat, so the final count ends a little lower.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run redis-benchmark?&lt;/strong&gt;
No. LoadFocus does not run redis-benchmark. Its JMeter test plans and k6 scripts are aimed at the web app or API in front of Redis.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools: wrk, hey, oha, ab and more&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench: how to benchmark and load test PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;How to use sysbench for CPU, disk and MySQL benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/ycsb/&quot;&gt;YCSB: workloads A-F, how to run it and its limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>How to use sysbench for CPU, disk and MySQL benchmarks</title><link>https://loadtest.app/sysbench/</link><guid isPermaLink="true">https://loadtest.app/sysbench/</guid><description>How sysbench works: CPU, memory and file I/O tests, OLTP scripts for MySQL and PostgreSQL, the options that matter, how to read the output, its limits.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;sysbench is an open source, multi-threaded benchmark tool built on LuaJIT that ships with CPU, memory, file I/O, thread and mutex tests, plus OLTP scripts for MySQL and PostgreSQL. Tests that need data follow the same lifecycle: &lt;code&gt;prepare&lt;/code&gt; creates the test files or tables, &lt;code&gt;run&lt;/code&gt; executes the benchmark for a set time or number of events, and &lt;code&gt;cleanup&lt;/code&gt; removes the data; the CPU and memory tests need only &lt;code&gt;run&lt;/code&gt;. It measures one machine or one database server, so it cannot tell you how your application behaves under real user traffic.&lt;/p&gt;
&lt;p&gt;We have not run sysbench for this guide. The facts come from its README and source code on GitHub&amp;#39;s master branch, checked on 31 October 2026, and the advice is ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;sysbench&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;GPLv2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Written in&lt;/td&gt;
&lt;td&gt;C, with LuaJIT for scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latest release&lt;/td&gt;
&lt;td&gt;1.0.20 (24 April 2020)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latest commit&lt;/td&gt;
&lt;td&gt;9 March 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cpu&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;fileio&lt;/code&gt;, &lt;code&gt;threads&lt;/code&gt;, &lt;code&gt;mutex&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database scripts&lt;/td&gt;
&lt;td&gt;Eight &lt;code&gt;oltp_*&lt;/code&gt; scripts, &lt;code&gt;bulk_insert&lt;/code&gt;, two &lt;code&gt;select_random_*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database drivers&lt;/td&gt;
&lt;td&gt;MySQL (default), PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platforms&lt;/td&gt;
&lt;td&gt;Linux, macOS; Windows through WSL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Is sysbench still maintained?&lt;/h2&gt;
&lt;p&gt;Only lightly. The latest release, 1.0.20, came out in April 2020; the master branch has had occasional commits since, none of them released, the most recent in March 2025.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/akopytov/sysbench/releases/tag/1.0.20&quot;&gt;1.0.20 release&lt;/a&gt; is dated 24 April 2020. The &lt;a href=&quot;https://github.com/akopytov/sysbench/commits/master&quot;&gt;commit history&lt;/a&gt; shows changes such as a uniform default for &lt;code&gt;--rand-type&lt;/code&gt; (2020), an upstream LuaJIT (2023) and MariaDB client support (2023), then four build commits on 9 March 2025 (checked on 31 October 2026). The README&amp;#39;s package repositories are updated on each release, so those changes need a build from source.&lt;/p&gt;
&lt;h2&gt;How does a sysbench run work?&lt;/h2&gt;
&lt;p&gt;You name a test and a command: &lt;code&gt;sysbench [options] [testname] [command]&lt;/code&gt;. The test is a built-in, a bundled Lua script or your own script, and the command is usually &lt;code&gt;prepare&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;cleanup&lt;/code&gt; or &lt;code&gt;help&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/akopytov/sysbench&quot;&gt;README&lt;/a&gt; says &lt;code&gt;prepare&lt;/code&gt; creates files for &lt;code&gt;fileio&lt;/code&gt; or fills the database for the OLTP scripts, &lt;code&gt;cleanup&lt;/code&gt; removes that data, and &lt;code&gt;sysbench &amp;lt;testname&amp;gt; help&lt;/code&gt; lists a test&amp;#39;s options. A disk test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sysbench fileio --file-total-size=8G prepare
sysbench fileio --file-total-size=8G \
  --file-test-mode=rndrw --time=300 run
sysbench fileio --file-total-size=8G cleanup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/tests/fileio/sb_fileio.c&quot;&gt;fileio source&lt;/a&gt; lists the modes &lt;code&gt;seqwr&lt;/code&gt;, &lt;code&gt;seqrewr&lt;/code&gt;, &lt;code&gt;seqrd&lt;/code&gt;, &lt;code&gt;rndrd&lt;/code&gt;, &lt;code&gt;rndwr&lt;/code&gt; and &lt;code&gt;rndrw&lt;/code&gt;, with 2 GB of files by default. Files that fit in memory can be served from the page cache, so make the set larger than RAM to measure the disk.&lt;/p&gt;
&lt;h2&gt;How do you benchmark CPU and memory with sysbench?&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;sysbench cpu run&lt;/code&gt; and &lt;code&gt;sysbench memory run&lt;/code&gt;. The CPU test reports events per second, where one event checks every number up to a limit for primality, and the memory test reports operations and MiB transferred per second.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sysbench cpu --cpu-max-prime=20000 \
  --threads=4 --time=60 run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/tests/cpu/sb_cpu.c&quot;&gt;CPU test&lt;/a&gt; sets &lt;code&gt;--cpu-max-prime&lt;/code&gt; to 10000 by default, and the &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/tests/memory/sb_memory.c&quot;&gt;memory test&lt;/a&gt; takes a block size, a total size, read or write operations and sequential or random access (checked on 31 October 2026). Compare only runs with identical settings.&lt;/p&gt;
&lt;h2&gt;How do you benchmark MySQL or PostgreSQL with sysbench?&lt;/h2&gt;
&lt;p&gt;Pick a bundled OLTP script, fill the tables with &lt;code&gt;prepare&lt;/code&gt;, run transactions from several threads with &lt;code&gt;run&lt;/code&gt;, then drop the tables with &lt;code&gt;cleanup&lt;/code&gt;. MySQL is the default driver; PostgreSQL needs &lt;code&gt;--db-driver=pgsql&lt;/code&gt; and a build with PostgreSQL support.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sysbench oltp_read_write --mysql-host=10.0.0.5 \
  --mysql-user=sbtest --mysql-password=secret \
  --tables=10 --table-size=1000000 prepare
sysbench oltp_read_write --mysql-host=10.0.0.5 \
  --mysql-user=sbtest --mysql-password=secret \
  --tables=10 --table-size=1000000 \
  --threads=16 --time=300 --report-interval=10 run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/lua/oltp_common.lua&quot;&gt;shared OLTP script&lt;/a&gt; creates one table of 10,000 rows unless you pass &lt;code&gt;--tables&lt;/code&gt; and &lt;code&gt;--table-size&lt;/code&gt;. In &lt;code&gt;oltp_read_write&lt;/code&gt;, each transaction runs 10 point selects, four range queries, two updates and a delete plus insert inside &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt; (checked on 31 October 2026). Make the data larger than the server&amp;#39;s buffer pool if you want disk reads in the result.&lt;/p&gt;
&lt;h2&gt;Which sysbench options matter most?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--threads&lt;/code&gt; and &lt;code&gt;--time&lt;/code&gt; set the size and length of a run, &lt;code&gt;--rate&lt;/code&gt; sets a target event rate, and &lt;code&gt;--report-interval&lt;/code&gt;, &lt;code&gt;--percentile&lt;/code&gt; and &lt;code&gt;--histogram&lt;/code&gt; decide what you see.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--threads&lt;/code&gt;: worker threads, 1 by default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--time&lt;/code&gt;: seconds, 10 by default, 0 for no limit. &lt;code&gt;--events&lt;/code&gt; caps the event count, and both limits apply, so pair it with &lt;code&gt;--time=0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--warmup-time&lt;/code&gt;: runs with statistics off first, so cold caches do not skew the result. Master only: the 1.0.20 release does not have it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--rate&lt;/code&gt;: average events per second across all threads; 0, the default, means as fast as possible.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--report-interval&lt;/code&gt;: per-interval figures every N seconds, to expose stalls.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--percentile&lt;/code&gt;: the latency percentile to report, 95 by default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--histogram&lt;/code&gt;: a latency histogram at the end, defined in &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/sb_logger.c&quot;&gt;sb_logger.c&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Does sysbench use a closed or an open workload model?&lt;/h2&gt;
&lt;p&gt;Closed by default: a fixed number of threads each start the next event when the previous one ends, so a slow server receives fewer events. With &lt;code&gt;--rate&lt;/code&gt;, events arrive on their own schedule, which is closer to an open model.&lt;/p&gt;
&lt;p&gt;Our article on the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains why that changes what a benchmark measures. In the default mode, a stall also delays the events that would have started during it, and those delays never reach the latency figures: &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt;. The &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/sysbench.c&quot;&gt;main source file&lt;/a&gt; shows that &lt;code&gt;--rate&lt;/code&gt; queues events at exponentially distributed intervals, adds each event&amp;#39;s queue wait to its latency, and stops with an &amp;quot;event queue is full&amp;quot; error when the threads cannot keep up (checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you read sysbench output?&lt;/h2&gt;
&lt;p&gt;Start with throughput: events per second, or transactions and queries per second for the OLTP scripts. Then read the latency block in milliseconds: minimum, average, maximum, the chosen percentile (95th by default) and the sum.&lt;/p&gt;
&lt;p&gt;The 1.0.20 release prints a &amp;quot;General statistics&amp;quot; block with the total time and total number of events in place of &amp;quot;Throughput&amp;quot;. The field names below come from the &lt;a href=&quot;https://github.com/akopytov/sysbench/blob/master/src/db_driver.c&quot;&gt;database report code&lt;/a&gt; and the main source file on master, with the values left out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL statistics:
    queries performed: read, write, other, total
    transactions, queries (N per sec.)
    ignored errors, reconnects (N per sec.)
Throughput:
    events/s (eps), time elapsed, total number of events
Latency (ms):
    min, avg, max, 95th percentile, sum
Threads fairness:
    events, execution time (avg/stddev)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 95th percentile ignores the slowest one in twenty events. Set &lt;code&gt;--percentile=99&lt;/code&gt; or add &lt;code&gt;--histogram&lt;/code&gt; when the tail matters; our guide to &lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;tail latency&lt;/a&gt; explains why.&lt;/p&gt;
&lt;h2&gt;When is sysbench the wrong tool?&lt;/h2&gt;
&lt;p&gt;When your question is about your application. A CPU score shows how fast one machine checks primes, and an OLTP run measures the database server with sysbench&amp;#39;s own tables and queries, without your schema, connection pool or network path.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;pgbench&lt;/strong&gt; for PostgreSQL-native tests: the &lt;a href=&quot;https://www.postgresql.org/docs/current/pgbench.html&quot;&gt;PostgreSQL documentation&lt;/a&gt; describes a default scenario loosely based on TPC-B, plus custom script files (checked on 31 October 2026).&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;HammerDB&lt;/strong&gt; for TPC-C-style tests: its &lt;a href=&quot;https://www.hammerdb.com/document.html&quot;&gt;documentation&lt;/a&gt; covers the TPROC-C workload, derived from TPC-C, for SQL Server, Db2, Oracle, PostgreSQL, MariaDB and MySQL (checked on 31 October 2026).&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;JMeter&lt;/strong&gt; with its JDBC Request sampler to mix SQL and HTTP in one test plan; the &lt;a href=&quot;https://jmeter.apache.org/usermanual/build-db-test-plan.html&quot;&gt;database test plan guide&lt;/a&gt; says the driver &lt;code&gt;.jar&lt;/code&gt; goes into JMeter&amp;#39;s &lt;code&gt;lib&lt;/code&gt; folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run sysbench and cannot connect to MySQL or PostgreSQL itself. It &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sysbench&quot;&gt;runs JMeter test plans&lt;/a&gt; and k6 scripts from 25 cloud regions against the web or API layer in front of the database. A useful pairing is a sysbench run to size the database server, then a &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sysbench&quot;&gt;load test of the application&lt;/a&gt; to see whether real requests reach that limit.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is sysbench still maintained?&lt;/strong&gt;
Lightly. The latest release is 1.0.20 from 24 April 2020. The master branch has received occasional commits since, none of them in a release, the most recent on 9 March 2025.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What does the 95th percentile in sysbench output mean?&lt;/strong&gt;
It is the latency, in milliseconds, within which 95% of events finished. 95 is the default of the &lt;code&gt;--percentile&lt;/code&gt; option, and you can set another value such as 99.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can sysbench benchmark PostgreSQL?&lt;/strong&gt;
Yes. The bundled OLTP scripts run against PostgreSQL with &lt;code&gt;--db-driver=pgsql&lt;/code&gt; when sysbench was built with PostgreSQL support.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run sysbench?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts against websites and APIs from 25 cloud regions, and it does not connect to MySQL or PostgreSQL directly.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools: wrk, hey, oha, ab and more&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench: how to benchmark and load test PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/ycsb/&quot;&gt;YCSB: workloads A-F, how to run it and its limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/redis-benchmark/&quot;&gt;redis-benchmark: how to use it and read its numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tpc-h/&quot;&gt;TPC-H: what the benchmark measures and how to run it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>YCSB: workloads A-F, how to run it and its limits</title><link>https://loadtest.app/ycsb/</link><guid isPermaLink="true">https://loadtest.app/ycsb/</guid><description>YCSB explained: what the Yahoo! Cloud Serving Benchmark measures, core workloads A to F, load and run phases, how to read its output and where it stops.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;YCSB, the Yahoo! Cloud Serving Benchmark, is an open source Java tool that loads synthetic records into a datastore and then runs a fixed mix of reads, updates, inserts and scans against them while it records throughput and latency per operation. Its six core workloads, A to F, are standard mixes, so the same test can run against Cassandra, MongoDB, Redis, HBase and dozens of other stores through bindings. It measures the datastore on its own, so it says nothing about your application&amp;#39;s real queries or the latency your users see.&lt;/p&gt;
&lt;p&gt;We have not run YCSB for this article. The facts come from its GitHub repository, wiki and source code, checked on 31 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Operation mix&lt;/th&gt;
&lt;th&gt;Key distribution&lt;/th&gt;
&lt;th&gt;Example given in the wiki&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A: update heavy&lt;/td&gt;
&lt;td&gt;50% read, 50% update&lt;/td&gt;
&lt;td&gt;zipfian&lt;/td&gt;
&lt;td&gt;Session store recording recent actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: read mostly&lt;/td&gt;
&lt;td&gt;95% read, 5% update&lt;/td&gt;
&lt;td&gt;zipfian&lt;/td&gt;
&lt;td&gt;Photo tagging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: read only&lt;/td&gt;
&lt;td&gt;100% read&lt;/td&gt;
&lt;td&gt;zipfian&lt;/td&gt;
&lt;td&gt;User profile cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D: read latest&lt;/td&gt;
&lt;td&gt;95% read, 5% insert&lt;/td&gt;
&lt;td&gt;latest&lt;/td&gt;
&lt;td&gt;User status updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E: short ranges&lt;/td&gt;
&lt;td&gt;95% scan, 5% insert&lt;/td&gt;
&lt;td&gt;zipfian (scans of up to 100 records)&lt;/td&gt;
&lt;td&gt;Threaded conversations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F: read-modify-write&lt;/td&gt;
&lt;td&gt;50% read, 50% read-modify-write&lt;/td&gt;
&lt;td&gt;zipfian&lt;/td&gt;
&lt;td&gt;User database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The mixes come from the files in the &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/tree/master/workloads&quot;&gt;workloads directory&lt;/a&gt; and the examples from the &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/wiki/Core-Workloads&quot;&gt;Core Workloads page&lt;/a&gt; (checked on 31 October 2026). Each file ships with only 1,000 records and 1,000 operations, enough to debug a setup and far too few for a benchmark.&lt;/p&gt;
&lt;h2&gt;What is YCSB, and is it still maintained?&lt;/h2&gt;
&lt;p&gt;It is a workload generator plus a set of standard workloads for comparing key-value and &amp;quot;cloud serving&amp;quot; stores, released as open source by Yahoo! in 2010 under the Apache 2.0 licence. The last release dates from October 2019, and the code still receives small commits.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/wiki/Papers-and-Presentations&quot;&gt;papers page&lt;/a&gt; lists the original paper, &amp;quot;Benchmarking Cloud Serving Systems with YCSB&amp;quot; by Cooper, Silberstein, Tam, Ramakrishnan and Sears (ACM SoCC 2010). The &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/releases/tag/0.17.0&quot;&gt;0.17.0 release&lt;/a&gt; is dated 6 October 2019 and no later tag exists, while the &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/commits/master&quot;&gt;commit history&lt;/a&gt; shows 2026 work such as an option to keep going when an insert fails and a Cloud Spanner change on 20 October (checked on 31 October 2026). Those changes ship only in a build from source, which needs Maven 3.&lt;/p&gt;
&lt;h2&gt;How do you run a YCSB benchmark?&lt;/h2&gt;
&lt;p&gt;In two phases that share the same properties: &lt;code&gt;load&lt;/code&gt; inserts &lt;code&gt;recordcount&lt;/code&gt; records, then &lt;code&gt;run&lt;/code&gt; executes &lt;code&gt;operationcount&lt;/code&gt; operations from the workload&amp;#39;s mix. You name the binding for your database and pass the workload file with &lt;code&gt;-P&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;bin/ycsb load mongodb -s -P workloads/workloada \
  -p recordcount=1000000 \
  -p mongodb.url=mongodb://db1:27017/ycsb
bin/ycsb run mongodb -s -P workloads/workloada \
  -p recordcount=1000000 -p operationcount=5000000 \
  -p mongodb.url=mongodb://db1:27017/ycsb \
  -threads 32 -target 20000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/wiki/Running-a-Workload&quot;&gt;Running a Workload page&lt;/a&gt; covers the details that trip people up (checked on 31 October 2026). The client never creates tables, so you first map &lt;code&gt;usertable&lt;/code&gt; to whatever your store needs, such as a table in MySQL or a keyspace in Cassandra. Later &lt;code&gt;-P&lt;/code&gt; files override earlier ones, and &lt;code&gt;-p&lt;/code&gt; overrides them all. The run phase needs the same &lt;code&gt;recordcount&lt;/code&gt; as the load, because it picks keys from the full dataset. &lt;code&gt;-s&lt;/code&gt; prints a status line every 10 seconds. Because D and E insert records, the Core Workloads page suggests loading once with workload A&amp;#39;s file, running A, B, C, F and D, then deleting the data and reloading for E.&lt;/p&gt;
&lt;h2&gt;What do -threads and -target control?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;-threads&lt;/code&gt; sets how many client threads send operations (one by default), and &lt;code&gt;-target&lt;/code&gt; caps the total operations per second. Without a target each thread sends its next operation as soon as the last one returns, so a slower database simply receives less load.&lt;/p&gt;
&lt;p&gt;That is a closed loop, and our article on the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains what it hides. The wiki&amp;#39;s example of 10 threads and a target of 100 operations per second holds only while average latency stays under 100 ms; with too few threads the achieved rate falls below the target. That is &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law&lt;/a&gt; at work: threads divided by average latency caps the throughput. Running several targets and noting the latency at each gives you the curve from our guide to &lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;throughput vs latency&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How do you read YCSB output?&lt;/h2&gt;
&lt;p&gt;At the end of each phase the client prints an &lt;code&gt;[OVERALL]&lt;/code&gt; block with run time and throughput, then one block per operation type with the count, average, minimum, maximum and percentile latencies, plus a count per return code.&lt;/p&gt;
&lt;p&gt;These field names come from the 0.17.0 source code; the values are placeholders:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[OVERALL], RunTime(ms), &amp;lt;total&amp;gt;
[OVERALL], Throughput(ops/sec), &amp;lt;rate&amp;gt;
[READ], Operations, &amp;lt;count&amp;gt;
[READ], AverageLatency(us), &amp;lt;value&amp;gt;
[READ], 95thPercentileLatency(us), &amp;lt;value&amp;gt;
[READ], 99thPercentileLatency(us), &amp;lt;value&amp;gt;
[READ], Return=OK, &amp;lt;count&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The wiki&amp;#39;s sample output shows milliseconds, but the default HdrHistogram measurement in both 0.17.0 and the current source reports microseconds, &lt;code&gt;(us)&lt;/code&gt;. The &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties&quot;&gt;Core Properties page&lt;/a&gt; and release 0.17.0 set the default percentiles to 95 and 99, changeable with &lt;code&gt;hdrhistogram.percentiles&lt;/code&gt;; since April 2025 the master branch adds 50, so a source build also prints &lt;code&gt;50thPercentileLatency(us)&lt;/code&gt; (checked on 31 October 2026). The &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB&quot;&gt;README&lt;/a&gt; points you at P99 and beyond, warns that percentiles cannot be averaged, and suggests saving HDR histogram files with &lt;code&gt;hdrhistogram.fileoutput=true&lt;/code&gt; and merging them when several clients run.&lt;/p&gt;
&lt;h2&gt;Does YCSB account for coordinated omission?&lt;/h2&gt;
&lt;p&gt;Only when you ask for it, and only with &lt;code&gt;-target&lt;/code&gt; set. By default each latency is timed from the moment the operation was actually sent, so a stall that delays the operations queued behind it barely shows.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/blob/master/core/src/main/java/site/ycsb/measurements/Measurements.java&quot;&gt;measurements source&lt;/a&gt; has a &lt;code&gt;measurement.interval&lt;/code&gt; property that takes &lt;code&gt;op&lt;/code&gt; (the default), &lt;code&gt;intended&lt;/code&gt; or &lt;code&gt;both&lt;/code&gt; (checked on 31 October 2026). Under a target, each thread records the scheduled start of every operation; &lt;code&gt;intended&lt;/code&gt; times latency from that point, and &lt;code&gt;both&lt;/code&gt; adds &lt;code&gt;Intended-&lt;/code&gt; blocks such as &lt;code&gt;[Intended-READ]&lt;/code&gt; next to the usual ones. The wiki does not list &lt;code&gt;measurement.interval&lt;/code&gt;, so check the source you build. Our guide to &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt; explains why the scheduled start gives the honest number.&lt;/p&gt;
&lt;h2&gt;Which databases can YCSB benchmark?&lt;/h2&gt;
&lt;p&gt;Any store with a binding. The build file on the master branch lists 46 binding modules, among them Cassandra, MongoDB, Redis, HBase, DynamoDB, Cloud Spanner, RocksDB and a generic JDBC binding.&lt;/p&gt;
&lt;p&gt;The list is in the &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/blob/master/pom.xml&quot;&gt;pom.xml&lt;/a&gt;, and each binding directory has its own README with connection properties, such as &lt;code&gt;mongodb.url&lt;/code&gt; for the &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/tree/master/mongodb&quot;&gt;MongoDB binding&lt;/a&gt; (checked on 31 October 2026). The Running a Workload page says one client machine suits clusters of about 10 machines; beyond that you start several clients, and the &lt;a href=&quot;https://github.com/brianfrankcooper/YCSB/wiki/Running-a-Workload-in-Parallel&quot;&gt;Running a Workload in Parallel&lt;/a&gt; page splits the load between them with &lt;code&gt;insertstart&lt;/code&gt; and &lt;code&gt;insertcount&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;When is YCSB the wrong tool?&lt;/h2&gt;
&lt;p&gt;When your question is about your application. The core workload writes synthetic records, 10 fields with a length of 100 each by default, through simple key operations, so it cannot show how your own queries, indexes or API code behave under load.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For relational OLTP, &lt;a href=&quot;https://www.postgresql.org/docs/current/pgbench.html&quot;&gt;pgbench&lt;/a&gt; runs a TPC-B-like script against PostgreSQL and accepts your own scripts, and &lt;a href=&quot;https://github.com/akopytov/sysbench&quot;&gt;sysbench&lt;/a&gt; bundles OLTP-style Lua benchmarks (both checked on 31 October 2026).&lt;/li&gt;
&lt;li&gt;For latency as users see it, load the HTTP or API layer in front of the datastore. LoadFocus, which publishes this site, &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ycsb&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions against web and API endpoints. It does not run YCSB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What are the YCSB core workloads?&lt;/strong&gt;
Six standard mixes: A is 50% reads and 50% updates, B is 95% reads, C is read only, D favours the most recently inserted records, E runs short scans, and F does read-modify-write.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between ycsb load and ycsb run?&lt;/strong&gt;
The load phase inserts the number of records set by &lt;code&gt;recordcount&lt;/code&gt;. The run phase then executes &lt;code&gt;operationcount&lt;/code&gt; operations from the workload&amp;#39;s mix against those records and reports throughput and latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is YCSB still maintained?&lt;/strong&gt;
The last release, 0.17.0, came out on 6 October 2019. The repository still receives occasional commits, the most recent on 20 October 2026, so newer fixes require a build from source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run YCSB benchmarks?&lt;/strong&gt;
No. LoadFocus does not run YCSB. It runs JMeter test plans and k6 scripts aimed at web and API endpoints, the layer in front of the datastore.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench: how to benchmark and load test PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;How to use sysbench for CPU, disk and MySQL benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/redis-benchmark/&quot;&gt;redis-benchmark: how to use it and read its numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tpc-h/&quot;&gt;TPC-H: what the benchmark measures and how to run it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>pgbench: how to benchmark and load test PostgreSQL</title><link>https://loadtest.app/pgbench/</link><guid isPermaLink="true">https://loadtest.app/pgbench/</guid><description>pgbench explained: initialize with -s, run with -c, -j and -T, write custom scripts, throttle with -R and read tps, latency and failed transactions.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;pgbench is the benchmark program that ships with PostgreSQL: it runs the same SQL transaction over and over in several concurrent database sessions and reports transactions per second and latency. You create its test tables with &lt;code&gt;pgbench -i -s 50 mydb&lt;/code&gt;, then run something like &lt;code&gt;pgbench -c 10 -j 2 -T 300 mydb&lt;/code&gt; for ten clients on two threads for five minutes. It measures the database alone, so it says nothing about what users feel through the application in front of it.&lt;/p&gt;
&lt;p&gt;We have not run pgbench for this guide. The facts come from its page in the PostgreSQL 18 documentation, the version postgresql.org marks as current, checked on 31 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i&lt;/code&gt; / &lt;code&gt;-s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initialize the test tables / scale factor (100,000 accounts per unit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clients, that is concurrent database sessions (default 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-j&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Worker threads inside pgbench (default 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-T&lt;/code&gt; / &lt;code&gt;-t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run for N seconds, or N transactions per client (default 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-b&lt;/code&gt; / &lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A built-in script or your own file, with an optional &lt;code&gt;@weight&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Progress report every N seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-R&lt;/code&gt; / &lt;code&gt;-L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target rate in tps / latency limit in ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-r&lt;/code&gt; / &lt;code&gt;-l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-statement latencies / a log line per transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What does pgbench measure?&lt;/h2&gt;
&lt;p&gt;It runs a transaction script again and again in a set number of database sessions and reports the average transaction rate and latency. The default script is loosely based on the TPC-B banking benchmark.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.postgresql.org/docs/current/pgbench.html&quot;&gt;pgbench documentation&lt;/a&gt; says the default scenario is inspired by TPC-B and is &amp;quot;not actually TPC-B, hence the name&amp;quot;: the output labels it &lt;code&gt;TPC-B (sort of)&lt;/code&gt; (checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you set up a pgbench database?&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;pgbench -i&lt;/code&gt; against an existing database, with &lt;code&gt;-s&lt;/code&gt; for the scale factor. It creates four tables and drops any existing tables with the same names, so give it a database of its own.&lt;/p&gt;
&lt;p&gt;At scale 1 the tables hold 1 branch, 10 tellers, 100,000 accounts and an empty history table, and &lt;code&gt;-s&lt;/code&gt; multiplies those rows: &lt;code&gt;-s 100&lt;/code&gt; gives 10,000,000 rows in &lt;code&gt;pgbench_accounts&lt;/code&gt;. The documentation gives no size on disk, so measure it after initializing (checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;Which built-in scripts does pgbench include?&lt;/h2&gt;
&lt;p&gt;Three: &lt;code&gt;tpcb-like&lt;/code&gt; (the default), &lt;code&gt;simple-update&lt;/code&gt; and &lt;code&gt;select-only&lt;/code&gt;. You pick one with &lt;code&gt;-b&lt;/code&gt;, or with the shortcuts &lt;code&gt;-N&lt;/code&gt; and &lt;code&gt;-S&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The default runs seven commands per transaction: &lt;code&gt;BEGIN&lt;/code&gt;, an update and a select on a random account, teller and branch updates, a history insert, and &lt;code&gt;END&lt;/code&gt;. &lt;code&gt;simple-update&lt;/code&gt; drops the teller and branch updates, which avoids contention on those small tables and, in the documentation&amp;#39;s words, makes the test &amp;quot;even less like TPC-B&amp;quot;. &lt;code&gt;select-only&lt;/code&gt; issues only the select.&lt;/p&gt;
&lt;h2&gt;How do you write a custom pgbench script?&lt;/h2&gt;
&lt;p&gt;Put your SQL in a file, set variables with &lt;code&gt;\set&lt;/code&gt; and the random functions, and pass the file with &lt;code&gt;-f&lt;/code&gt;. Each run of the file counts as one transaction.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;\set aid random(1, 100000 * :scale)
\set delta random(-5000, 5000)
BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta
  WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A weight after &lt;code&gt;@&lt;/code&gt; sets how often each script is picked: &lt;code&gt;-f update.sql@9 -b select-only@1&lt;/code&gt; picks the file about nine times in ten. The documentation also lists &lt;code&gt;random_exponential&lt;/code&gt;, &lt;code&gt;random_gaussian&lt;/code&gt; and &lt;code&gt;random_zipfian&lt;/code&gt;, which can make a few rows hot. &lt;code&gt;\sleep&lt;/code&gt; adds a pause, the database version of &lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;think time&lt;/a&gt;. When you run only your own files, pass the database&amp;#39;s scale with &lt;code&gt;-s&lt;/code&gt;, or &lt;code&gt;:scale&lt;/code&gt; stays 1 and the script above touches only the first 100,000 accounts. With your own tables, leave out &lt;code&gt;-i&lt;/code&gt; and add &lt;code&gt;-n&lt;/code&gt; so pgbench does not vacuum the standard ones (checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;Which options make pgbench behave like a load test?&lt;/h2&gt;
&lt;p&gt;Concurrency comes from &lt;code&gt;-c&lt;/code&gt; and &lt;code&gt;-j&lt;/code&gt;, duration from &lt;code&gt;-T&lt;/code&gt;, and the shape of the load from &lt;code&gt;-R&lt;/code&gt;. Without &lt;code&gt;-R&lt;/code&gt;, each client starts its next transaction as soon as the last one ends.&lt;/p&gt;
&lt;p&gt;That default is a closed model, explained in our article on the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt;. With &lt;code&gt;-R&lt;/code&gt;, pgbench starts transactions on a Poisson-distributed schedule that moves forward from the client&amp;#39;s start time, whatever happened to earlier transactions. Reported latency then counts from each scheduled start, so it includes the schedule lag, and the average and maximum lag are shown separately: the correction our &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt; article describes. The client count stays fixed, so a lag that keeps growing means the system cannot process that rate with the chosen clients and threads: lower &lt;code&gt;-R&lt;/code&gt; or add clients.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--latency-limit&lt;/code&gt; (&lt;code&gt;-L&lt;/code&gt;) counts transactions slower than a limit in milliseconds as late; with &lt;code&gt;-R&lt;/code&gt; it also skips transactions already too far behind schedule and reports them as skipped. &lt;code&gt;-M prepared&lt;/code&gt; uses prepared statements, which the documentation says runs faster than the other modes (checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you read pgbench output?&lt;/h2&gt;
&lt;p&gt;The summary lists the settings, transactions processed and failed, average latency, initial connection time and tps. The shape below comes from the documentation, numbers left out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;transaction type: &amp;lt;builtin: TPC-B (sort of)&amp;gt;
number of transactions actually processed: ...
number of failed transactions: ... (...%)
latency average = ... ms
latency stddev = ... ms   (only with -P, -R or -L)
initial connection time = ... ms
tps = ... (without initial connection time)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Failed transactions are serialization or deadlock errors, which &lt;code&gt;--max-tries&lt;/code&gt; can retry. Other SQL errors abort that client and make the exit status 2. The summary has no percentiles, so for &lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;tail latency&lt;/a&gt; log each transaction with &lt;code&gt;-l&lt;/code&gt;, which records its time in microseconds, and compute p95 or p99 yourself.&lt;/p&gt;
&lt;p&gt;Unless you pass &lt;code&gt;-P&lt;/code&gt;, &lt;code&gt;-R&lt;/code&gt; or &lt;code&gt;-L&lt;/code&gt;, the summary&amp;#39;s average latency is not measured: pgbench derives it from run time and client count, &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law&lt;/a&gt; run backwards, failures included. Add &lt;code&gt;-P&lt;/code&gt; when latency matters.&lt;/p&gt;
&lt;h2&gt;How do you get pgbench numbers you can trust?&lt;/h2&gt;
&lt;p&gt;Run for minutes, repeat, keep the scale factor at least as large as the client count, and watch vacuum. If pgbench becomes the bottleneck, move it to another machine.&lt;/p&gt;
&lt;p&gt;The first guideline in the documentation&amp;#39;s &amp;quot;Good Practices&amp;quot; section is &amp;quot;never believe any test that runs for only a few seconds&amp;quot; and adds that reproducible numbers can take hours (checked on 31 October 2026). With the default script, a scale below the client count mostly measures update contention, because &lt;code&gt;pgbench_branches&lt;/code&gt; has only &lt;code&gt;-s&lt;/code&gt; rows and every transaction updates one. Dead rows and autovacuum shift results between runs. With many sessions pgbench can saturate its own host: run it on a separate machine with low network latency, or spread several instances over several machines.&lt;/p&gt;
&lt;h2&gt;When is pgbench the wrong tool?&lt;/h2&gt;
&lt;p&gt;When the question is about users, another database or a standard OLTP mix. pgbench measures PostgreSQL alone, without your connection pool, ORM, cache or HTTP layer.&lt;/p&gt;
&lt;p&gt;To see what users feel, load the HTTP or API layer in front of it. LoadFocus, which publishes this site, does not run pgbench and cannot connect to PostgreSQL directly: it &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pgbench&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions against web and API endpoints, and its &lt;a href=&quot;https://loadfocus.com/templates/load-testing-for-database-query-performance?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pgbench&quot;&gt;template for the endpoints behind heavy queries&lt;/a&gt; is a starting point.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;sysbench&lt;/strong&gt; instead for MySQL: its &lt;a href=&quot;https://github.com/akopytov/sysbench&quot;&gt;repository&lt;/a&gt; describes a scriptable multi-threaded benchmark tool with OLTP-like database tests. Choose &lt;strong&gt;HammerDB&lt;/strong&gt; for a TPC-C style mix: its &lt;a href=&quot;https://www.hammerdb.com/docs/ch03.html&quot;&gt;documentation&lt;/a&gt; describes TPROC-C, a workload derived from TPC-C, which it runs on &lt;a href=&quot;https://www.hammerdb.com/&quot;&gt;PostgreSQL and other engines&lt;/a&gt;. Choose &lt;strong&gt;JMeter&lt;/strong&gt; with its &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html&quot;&gt;JDBC Request sampler&lt;/a&gt; to mix SQL and HTTP in one test plan on your own machines (all checked on 31 October 2026).&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What does the -s option do in pgbench?&lt;/strong&gt;
It sets the scale factor at initialization. Each unit adds 100,000 rows to pgbench_accounts, so -s 100 creates 10,000,000 rows. With the default script, keep it at least as large as your client count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between -c and -j in pgbench?&lt;/strong&gt;
The -c option sets the number of simulated clients, each a concurrent database session. The -j option sets how many worker threads inside pgbench share those clients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run pgbench?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts and has no direct PostgreSQL connection, so with it you load the web or API layer that sits in front of the database.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;How to use sysbench for CPU, disk and MySQL benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/ycsb/&quot;&gt;YCSB: workloads A-F, how to run it and its limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/redis-benchmark/&quot;&gt;redis-benchmark: how to use it and read its numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tpc-h/&quot;&gt;TPC-H: what the benchmark measures and how to run it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>NBomber load testing: a review of the .NET framework</title><link>https://loadtest.app/nbomber-load-testing/</link><guid isPermaLink="true">https://loadtest.app/nbomber-load-testing/</guid><description>NBomber load testing reviewed: C# and F# scenarios, inject and constant simulations, thresholds, the HTTP plugin, cluster mode and its company licence.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;NBomber is a load testing framework for .NET: you write each test as a C# or F# scenario, add it to an ordinary project and run it like any other program. NBomber load testing suits teams that already build in .NET and want their load tests in the same language, the same repository and the same test runner. NBomber is free for personal and academic use only; a company needs a paid licence, and that one fact decides more evaluations than any feature.&lt;/p&gt;
&lt;p&gt;We have not run NBomber for this review. The facts come from NBomber&amp;#39;s documentation, pricing page, GitHub repository and NuGet package, checked on 21 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;NBomber&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Free for personal and academic use; paid for organisations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price for a company&lt;/td&gt;
&lt;td&gt;Business $1,500 a year, Enterprise $2,500 a year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current release&lt;/td&gt;
&lt;td&gt;6.7.1 (16 October 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;.NET Standard 2.1, so .NET Core 3.0 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written as&lt;/td&gt;
&lt;td&gt;C# or F# scenarios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;Inject (arrival rate) or constant copies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass/fail checks&lt;/td&gt;
&lt;td&gt;Thresholds during the run, asserts on final stats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;NBomber Cluster, Enterprise licence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Who makes NBomber, and is it free?&lt;/h2&gt;
&lt;p&gt;NBomber LLC develops it, with the source on GitHub. It is free for personal, hobby and academic use, and the licence page states plainly that NBomber is not free for organisational use.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://nbomber.com/docs/getting-started/license/&quot;&gt;licence page&lt;/a&gt; counts benchmarks, tutorials and review content as personal use. Inside a company you need a Business or Enterprise licence, issued per organisation rather than per developer seat, and with no limit on instances or parallel runs. The &lt;a href=&quot;https://nbomber.com/pricing&quot;&gt;pricing page&lt;/a&gt; lists Business at $1,500 a year and Enterprise at $2,500 a year, both with a 30-day money-back guarantee. The licence key goes into the runner with &lt;code&gt;.WithLicense(...)&lt;/code&gt; or a &lt;code&gt;--license&lt;/code&gt; argument, and NBomber needs internet access to validate it, with offline validation available on request by email (checked on 21 October 2026).&lt;/p&gt;
&lt;p&gt;The latest release is &lt;a href=&quot;https://github.com/PragmaticFlow/NBomber/releases/tag/v6.7.1&quot;&gt;v6.7.1&lt;/a&gt;, published on 16 October 2026, after 6.7.0 four days earlier (checked on 21 October 2026).&lt;/p&gt;
&lt;h2&gt;How does an NBomber scenario work?&lt;/h2&gt;
&lt;p&gt;A scenario is a name plus an async function that NBomber times on every call. You attach load simulations and thresholds to it, register it with &lt;code&gt;NBomberRunner&lt;/code&gt; and run the program.&lt;/p&gt;
&lt;p&gt;This test skips the warm-up, ramps the arrival rate to 50 new scenario copies a second over two minutes, holds it for ten minutes, and exits with code 1 if 1% or more of requests failed. It uses the &lt;code&gt;NBomber.Http&lt;/code&gt; plugin for the request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var http = Http.CreateDefaultClient();

var scenario = Scenario.Create(&amp;quot;browse&amp;quot;, async context =&amp;gt;
{
    var request = Http.CreateRequest(&amp;quot;GET&amp;quot;,
        &amp;quot;https://staging.example.com/products&amp;quot;);
    return await Http.Send(http, request);
})
.WithoutWarmUp()
.WithLoadSimulations(
    Simulation.RampingInject(rate: 50,
        interval: TimeSpan.FromSeconds(1),
        during: TimeSpan.FromMinutes(2)),
    Simulation.Inject(rate: 50,
        interval: TimeSpan.FromSeconds(1),
        during: TimeSpan.FromMinutes(10)))
.WithThresholds(
    Threshold.Create(s =&amp;gt; s.Fail.Request.Percent &amp;lt; 1));

var result = NBomberRunner
    .RegisterScenarios(scenario).Run();
return result.Thresholds.Any(t =&amp;gt; t.IsFailed) ? 1 : 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://nbomber.com/docs/protocols/http/http-client/&quot;&gt;HTTP client page&lt;/a&gt; recommends &lt;code&gt;Http.CreateDefaultClient()&lt;/code&gt; over a plain &lt;code&gt;new HttpClient()&lt;/code&gt; and says the plugin tracks status codes and data transfer (checked on 21 October 2026). A threshold on its own only records the failure in the report, so the last line turns it into an exit code a pipeline can read. Without &lt;code&gt;.WithoutWarmUp()&lt;/code&gt;, NBomber first runs a 30-second warm-up with the scenario&amp;#39;s own simulations, per the &lt;a href=&quot;https://nbomber.com/docs/nbomber/scenario/&quot;&gt;scenario page&lt;/a&gt; (checked on 21 October 2026). Because the scenario is plain C#, anything a .NET library can call can be load tested the same way.&lt;/p&gt;
&lt;h2&gt;What load models does NBomber offer?&lt;/h2&gt;
&lt;p&gt;Both kinds. The inject simulations start new copies at a set rate whatever earlier copies are doing, which is an open model; the constant simulations keep a fixed number of copies running, which is a closed one.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://nbomber.com/docs/nbomber/load-simulation/&quot;&gt;load simulation page&lt;/a&gt; lists them (checked on 21 October 2026):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open:&lt;/strong&gt; &lt;code&gt;Inject&lt;/code&gt;, &lt;code&gt;RampingInject&lt;/code&gt;, &lt;code&gt;InjectRandom&lt;/code&gt; and &lt;code&gt;IterationsForInject&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Closed:&lt;/strong&gt; &lt;code&gt;KeepConstant&lt;/code&gt;, &lt;code&gt;RampingConstant&lt;/code&gt; and &lt;code&gt;IterationsForConstant&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timing:&lt;/strong&gt; &lt;code&gt;Pause&lt;/code&gt;, which delays a scenario&amp;#39;s start or pauses it for a set time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our article on the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains which one matches your traffic. A public website usually behaves like the open model; a fixed pool of internal clients, like the closed one.&lt;/p&gt;
&lt;h2&gt;How do thresholds and pass/fail checks work?&lt;/h2&gt;
&lt;p&gt;Runtime thresholds check the running statistics, every 5 seconds by default, and appear in the HTML report. Asserts, written in xUnit or NUnit, check the final statistics after the run, and a failed assert ends the run with a failed status.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://nbomber.com/docs/nbomber/asserts_and_thresholds/&quot;&gt;thresholds page&lt;/a&gt; shows checks on error percentage, status codes, latency percentiles (p50, p75, p95, p99 and max), requests per second and bytes transferred. A threshold does not stop the test by default; &lt;code&gt;abortWhenErrorCount&lt;/code&gt; makes it stop after a set number of failed checks (checked on 21 October 2026). The page does not state an exit code, which is why the sample above returns one from the result. Our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; covers which limits to set.&lt;/p&gt;
&lt;h2&gt;Which protocols can NBomber test?&lt;/h2&gt;
&lt;p&gt;Any protocol a .NET library can speak, because NBomber only times the code you give it. Ready-made plugins cover HTTP, WebSockets, MQTT and AMQP, plus a WebBrowser option.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://nbomber.com/docs/getting-started/overview/&quot;&gt;overview page&lt;/a&gt; describes the framework as having no dependency on a protocol, and the pricing page lists those plugins in every plan, including the free one. That also covers databases and message queues you call through their own .NET clients (checked on 21 October 2026). The trade-off is that outside the plugins you write and maintain the client code yourself.&lt;/p&gt;
&lt;h2&gt;How do you scale out an NBomber test?&lt;/h2&gt;
&lt;p&gt;With NBomber Cluster: one Coordinator orchestrates the run and Agents execute the scenarios, all talking through a NATS message broker. Cluster is part of the Enterprise licence.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://nbomber.com/docs/cluster/overview/&quot;&gt;cluster overview&lt;/a&gt; explains the roles, and the licence page says developers can try it locally with a Local Dev Cluster before buying; the &lt;a href=&quot;https://nbomber.com/docs/cluster/local-dev-cluster/&quot;&gt;Local Dev Cluster page&lt;/a&gt; limits it to 1 Coordinator and 2 Agents and stops each run after one minute (checked on 21 October 2026). NBomber Studio&amp;#39;s Kubernetes integration, also Enterprise-only, provisions NATS and the Agent pods for you. Without Cluster, one NBomber process runs on one machine; the &lt;a href=&quot;https://nbomber.com/docs/getting-started/system-requirements/&quot;&gt;system requirements page&lt;/a&gt; suggests 4 to 8 CPU cores for high load.&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run NBomber tests. It &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=nbomber-load-testing&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions.&lt;/p&gt;
&lt;h2&gt;What does NBomber do well, and where are its limits?&lt;/h2&gt;
&lt;p&gt;It is strong for .NET teams: tests are C# or F# code, any .NET client becomes a load test, and both load models are built in. Its limits are the company licence, a single-language ecosystem and paid distributed runs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests live next to the application code, with the same IDE, packages and reviews.&lt;/li&gt;
&lt;li&gt;Thresholds are built in, and asserts run inside the xUnit or NUnit tests you already have.&lt;/li&gt;
&lt;li&gt;Commercial use needs a licence, even for a single developer.&lt;/li&gt;
&lt;li&gt;Distributed runs need Enterprise and a NATS broker.&lt;/li&gt;
&lt;li&gt;Non-.NET testers cannot pick it up without learning C# or F#.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When should you choose k6, Gatling or JMeter instead?&lt;/h2&gt;
&lt;p&gt;Choose another tool when your team does not write .NET, or when the licence cost does not fit.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;k6&lt;/strong&gt; instead when you want JavaScript scripts and an open source tool with no licence fee for company use: its &lt;a href=&quot;https://github.com/grafana/k6&quot;&gt;repository&lt;/a&gt; is AGPL-3.0 (checked on 21 October 2026). LoadFocus &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=nbomber-load-testing&quot;&gt;runs k6 scripts&lt;/a&gt; from 25 cloud regions, though it runs arrival-rate scenarios with a fixed number of users. Choose &lt;strong&gt;Gatling&lt;/strong&gt; instead for a code-first JVM tool; our &lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling review&lt;/a&gt; covers it. Choose &lt;strong&gt;JMeter&lt;/strong&gt; instead when testers prefer a GUI or you need JDBC, JMS or LDAP samplers out of the box. The &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; overview sets those side by side.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is NBomber free for commercial use?&lt;/strong&gt;
No. NBomber is free for personal and academic use only. A company needs a Business licence at $1,500 a year or an Enterprise licence at $2,500 a year.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which .NET versions does NBomber support?&lt;/strong&gt;
It targets .NET Standard 2.1, so it runs on .NET Core 3.0 and every later .NET version. The documentation recommends .NET 10.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run NBomber tests?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To run NBomber on more than one machine, use NBomber Cluster, which needs an Enterprise licence.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling load testing: a review of the JVM tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tsung-load-testing/&quot;&gt;Tsung load testing: a review of the Erlang tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Tsung load testing: a review of the Erlang tool</title><link>https://loadtest.app/tsung-load-testing/</link><guid isPermaLink="true">https://loadtest.app/tsung-load-testing/</guid><description>Tsung load testing reviewed: XML scenarios, arrival phases, HTTP, XMPP, database and MQTT plugins, distributed clients, reports and its 2023 last release.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Tsung is an open source load testing tool written in Erlang that describes each test in an XML file and can spread the load over many client machines. Tsung load testing still makes sense for protocols few tools cover, such as XMPP, LDAP or PostgreSQL at the wire level, and for very large user counts on modest hardware. Tsung&amp;#39;s last release came out in March 2023, so for new HTTP or API work most teams are better served by k6, Gatling or JMeter.&lt;/p&gt;
&lt;p&gt;We have not run Tsung for this review. The facts come from Tsung&amp;#39;s documentation and GitHub repository, checked on 21 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Tsung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;GPL-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Written in&lt;/td&gt;
&lt;td&gt;Erlang (needs Erlang/OTP R16B03 or later)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latest release&lt;/td&gt;
&lt;td&gt;1.8.0 (2 March 2023)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written as&lt;/td&gt;
&lt;td&gt;XML configuration files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;New users per arrival phase (open)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocols&lt;/td&gt;
&lt;td&gt;HTTP, WebDAV, XMPP, PostgreSQL, MySQL, LDAP, WebSocket, AMQP, MQTT, raw TCP/UDP (some experimental)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;Client machines over passwordless SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reports&lt;/td&gt;
&lt;td&gt;Live dashboard on port 8091, &lt;code&gt;tsung_stats.pl&lt;/code&gt; HTML report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Who maintains Tsung, and is it still active?&lt;/h2&gt;
&lt;p&gt;ProcessOne hosts the project on GitHub under GPL-2.0. It is maintained but quiet: the last release, 1.8.0, is from March 2023, and changes since then are occasional merged pull requests.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/processone/tsung&quot;&gt;repository&lt;/a&gt; shows its most recent commit on 2 March 2026, a pull request that replaces deprecated calls, and the &lt;a href=&quot;https://github.com/processone/tsung/releases/tag/v1.8.0&quot;&gt;v1.8.0 tag&lt;/a&gt; is dated 2 March 2023 (checked on 21 October 2026). The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/&quot;&gt;documentation&lt;/a&gt; still carries the 1.7.0 title, so check the repository when a detail looks out of date. A quiet project also means fewer answers when something goes wrong.&lt;/p&gt;
&lt;h2&gt;How does a Tsung test work?&lt;/h2&gt;
&lt;p&gt;A test is one XML file with four main parts: the client machines, the target servers, the load phases and the user sessions. Each new user picks a session by its probability and runs its requests in order.&lt;/p&gt;
&lt;p&gt;This fragment starts five new users a second for ten minutes, and each user loads a product list, pauses about two seconds and posts to a cart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;load&amp;gt;
  &amp;lt;arrivalphase phase=&amp;quot;1&amp;quot; duration=&amp;quot;10&amp;quot; unit=&amp;quot;minute&amp;quot;&amp;gt;
    &amp;lt;users arrivalrate=&amp;quot;5&amp;quot; unit=&amp;quot;second&amp;quot;/&amp;gt;
  &amp;lt;/arrivalphase&amp;gt;
&amp;lt;/load&amp;gt;
&amp;lt;sessions&amp;gt;
  &amp;lt;session name=&amp;quot;browse&amp;quot; probability=&amp;quot;100&amp;quot;
           type=&amp;quot;ts_http&amp;quot;&amp;gt;
    &amp;lt;request&amp;gt;&amp;lt;http url=&amp;quot;/products&amp;quot; method=&amp;quot;GET&amp;quot;/&amp;gt;&amp;lt;/request&amp;gt;
    &amp;lt;thinktime value=&amp;quot;2&amp;quot; random=&amp;quot;true&amp;quot;/&amp;gt;
    &amp;lt;request&amp;gt;&amp;lt;http url=&amp;quot;/cart&amp;quot; method=&amp;quot;POST&amp;quot;
                   contents=&amp;quot;sku=A-100&amp;quot;/&amp;gt;&amp;lt;/request&amp;gt;
  &amp;lt;/session&amp;gt;
&amp;lt;/sessions&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/conf-sessions.html&quot;&gt;sessions page&lt;/a&gt; says the session probabilities must add up to 100 (or, since 1.5.0, you can use weights), and that a random think time follows an exponential distribution around its value unless you give a &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; range (checked on 21 October 2026). For HTTP and PostgreSQL there is a proxy recorder that turns a browser or client session into this XML.&lt;/p&gt;
&lt;h2&gt;What load model does Tsung use?&lt;/h2&gt;
&lt;p&gt;An open model. Each arrival phase starts new users at a set interval or rate, whatever the users already running are doing, and a phase ends when its time is up even if its sessions have not finished.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/conf-load.html&quot;&gt;load page&lt;/a&gt; lists the controls: &lt;code&gt;interarrival&lt;/code&gt; or &lt;code&gt;arrivalrate&lt;/code&gt; per phase, &lt;code&gt;maxnumber&lt;/code&gt; to cap the users a phase starts, a &lt;code&gt;duration&lt;/code&gt; on the whole load to stop the test at a fixed time, and &lt;code&gt;wait_all_sessions_end&lt;/code&gt; to hold the next phase until the current users finish (checked on 21 October 2026). Our articles on the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; and on &lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;think time&lt;/a&gt; explain how those choices change what a test measures.&lt;/p&gt;
&lt;h2&gt;Which protocols can Tsung test?&lt;/h2&gt;
&lt;p&gt;More than most load testing tools: HTTP (including SOAP), WebDAV, XMPP, PostgreSQL, MySQL, LDAP, WebSocket, AMQP, MQTT and raw TCP, UDP or SSL. The documentation marks the MySQL, WebSocket, AMQP and MQTT plugins as experimental, with AMQP 0.9.1 and MQTT 3.1 only.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/features.html&quot;&gt;features page&lt;/a&gt; describes each plugin (checked on 21 October 2026). XMPP and PostgreSQL are where Tsung has long had few rivals: it speaks the protocol itself, so you test the server rather than a client library. For real-time apps, our roundup of &lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools&lt;/a&gt; puts Tsung next to newer options.&lt;/p&gt;
&lt;h2&gt;How do you scale a Tsung test across machines?&lt;/h2&gt;
&lt;p&gt;You list the client machines in the XML, and the controller starts the load on them over SSH without a password, or rsh. Each client can take a weight, a user limit and a CPU count.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/conf-client-server.html&quot;&gt;clients and server page&lt;/a&gt; explains that Tsung runs one Erlang virtual machine per CPU you declare with &lt;code&gt;cpu&lt;/code&gt; on each client (one by default) and starts a new one when a VM reaches &lt;code&gt;maxusers&lt;/code&gt;. It can also take client machines from a batch scheduler such as PBS, LSF or OAR. The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/installation.html&quot;&gt;installation page&lt;/a&gt; lists Erlang/OTP R16B03 or later, plus optional Perl 5 and gnuplot, or Python and matplotlib, for the graphs (checked on 21 October 2026).&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run Tsung files. It &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tsung-load-testing&quot;&gt;runs JMeter test plans&lt;/a&gt; and k6 scripts from 25 cloud regions.&lt;/p&gt;
&lt;h2&gt;How do you read Tsung results?&lt;/h2&gt;
&lt;p&gt;Two ways: a live dashboard served by the controller on port 8091, and an HTML report built after the run with the &lt;code&gt;tsung_stats.pl&lt;/code&gt; script.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://tsung.readthedocs.io/en/latest/reports.html&quot;&gt;reports page&lt;/a&gt; says the dashboard has existed since 1.6.0, shows the current status and builds graphs while the test runs, and stops when the test ends unless you start Tsung with &lt;code&gt;-k&lt;/code&gt; (checked on 21 October 2026). The report covers response and connection times, transactions, requests per second and error codes, and can include server CPU, memory and network figures collected through Erlang agents, SNMP or munin. The documentation describes no pass/fail threshold, so a CI pipeline has to parse the results itself; our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; covers what to check.&lt;/p&gt;
&lt;h2&gt;What does Tsung do well, and where are its limits?&lt;/h2&gt;
&lt;p&gt;It is strong on protocol breadth, open-model arrival phases and many users per machine, which the documentation puts at thousands per CPU. Its limits are a quiet release history, XML-only scripting and no built-in pass/fail checks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XMPP, LDAP and wire-level database tests without writing client code.&lt;/li&gt;
&lt;li&gt;Distribution over plain SSH, with no broker or extra service.&lt;/li&gt;
&lt;li&gt;Server monitoring in the same report.&lt;/li&gt;
&lt;li&gt;Logic beyond the XML means Erlang, which few teams write.&lt;/li&gt;
&lt;li&gt;Erlang must be installed on the controller and every client.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When should you choose k6, Gatling or JMeter instead?&lt;/h2&gt;
&lt;p&gt;Choose another tool when you test HTTP or APIs and want an active project with scripts in a mainstream language.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;k6&lt;/strong&gt; instead for JavaScript scripts with &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt; built into the script and an active project (checked on 21 October 2026). Choose &lt;strong&gt;Gatling&lt;/strong&gt; instead for code-first simulations on the JVM; our &lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling review&lt;/a&gt; covers it. Choose &lt;strong&gt;JMeter&lt;/strong&gt; instead when testers want a GUI or you need JDBC, JMS or LDAP samplers with a large plugin ecosystem. LoadFocus &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tsung-load-testing&quot;&gt;runs JMeter and k6&lt;/a&gt; from 25 cloud regions, and the &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; overview sets these tools side by side.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Tsung still maintained?&lt;/strong&gt;
Yes, but quietly. The latest release, 1.8.0, came out on 2 March 2023, and the repository still receives occasional merged changes, most recently in March 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which protocols does Tsung support?&lt;/strong&gt;
HTTP and SOAP, WebDAV, XMPP, PostgreSQL, LDAP and raw TCP, UDP or SSL, plus MySQL, WebSocket, AMQP and MQTT, which the documentation marks as experimental.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Tsung tests?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To spread a Tsung test over several machines, list them as clients in the XML file.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools: six options compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling load testing: a review of the JVM tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/nbomber-load-testing/&quot;&gt;NBomber load testing: a review of the .NET framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench: how to benchmark and load test PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;How to use sysbench for CPU, disk and MySQL benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Azure Load Testing: a review of Microsoft&apos;s service</title><link>https://loadtest.app/azure-load-testing/</link><guid isPermaLink="true">https://loadtest.app/azure-load-testing/</guid><description>Azure Load Testing reviewed: JMeter and Locust tests, quick URL tests, engines and limits, regions, private endpoints, pricing per virtual user hour, CI.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Azure Load Testing is Microsoft&amp;#39;s managed service for running JMeter and Locust tests at scale from Azure, billed per virtual user hour. It is a strong fit when the system you test runs on Azure, because it can pull server metrics from Azure Monitor and reach private endpoints inside a virtual network. Teams that write k6 scripts, have no Azure subscription or test from outside Azure regions will find other services simpler.&lt;/p&gt;
&lt;p&gt;We have not run Azure Load Testing for this review. The facts come from Microsoft&amp;#39;s documentation and pricing page, checked on 19 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Azure Load Testing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Managed service in the Azure App Testing hub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test frameworks&lt;/td&gt;
&lt;td&gt;Apache JMeter and Locust only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No-code option&lt;/td&gt;
&lt;td&gt;URL-based quick test, up to five HTTP requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale per run&lt;/td&gt;
&lt;td&gt;Up to 400 engine instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest test&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load origin&lt;/td&gt;
&lt;td&gt;The resource&amp;#39;s region, or up to 5 regions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private endpoints&lt;/td&gt;
&lt;td&gt;Yes, through virtual network injection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$0.15 per virtual user hour, $0.06 after 10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results kept&lt;/td&gt;
&lt;td&gt;6 months; client metrics 365 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is Azure Load Testing?&lt;/h2&gt;
&lt;p&gt;It is a fully managed service that runs your load test on Microsoft&amp;#39;s test engines, so you never build or patch load generators. You create a load testing resource in a chosen Azure region, add tests to it and start runs from the portal, the Azure CLI or a pipeline.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/overview-what-is-azure-load-testing&quot;&gt;overview page&lt;/a&gt; says the application under test can live anywhere: in Azure, on-premises or in another cloud. Microsoft now lists the service inside the Azure App Testing hub, next to Playwright Workspaces; the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/quickstart-create-run-load-test-with-locust&quot;&gt;Locust quickstart&lt;/a&gt; creates the resource from that hub (checked on 19 October 2026).&lt;/p&gt;
&lt;h2&gt;Which test types and tools does it support?&lt;/h2&gt;
&lt;p&gt;Three ways in: a URL-based quick test, an uploaded JMeter script, or an uploaded Locust script. The docs state plainly that it does not support any other testing framework.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/quickstart-create-and-run-load-test&quot;&gt;quick test&lt;/a&gt; takes up to five HTTP requests and a target number of virtual users or requests per second. Each engine handles up to 250 virtual users, so 400 users run on two engines with 200 each.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;JMeter&lt;/strong&gt;, you can use plugins from jmeter-plugins.org or upload your own, and the overview says every protocol JMeter supports works, such as a database connection or a message queue. With &lt;strong&gt;Locust&lt;/strong&gt;, you can add Locust plugins and other Python libraries. Locust&amp;#39;s own documentation points to the service as its &lt;a href=&quot;https://docs.locust.io/en/stable/hosted-load-testing.html&quot;&gt;hosted option&lt;/a&gt;, and our &lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust review&lt;/a&gt; covers the tool itself. k6, Gatling and Artillery scripts do not run here (checked on 19 October 2026).&lt;/p&gt;
&lt;h2&gt;How do engines, virtual users and limits work?&lt;/h2&gt;
&lt;p&gt;You scale a test by adding engine instances. For JMeter the total load is the users in your test plan multiplied by the number of engines; for Locust you set the total users and pick how many engines generate them.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/concept-load-testing-concepts&quot;&gt;concepts page&lt;/a&gt; gives both formulas, and the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/resource-limits-quotas-capacity&quot;&gt;service limits page&lt;/a&gt; sets the ceilings per region and subscription (checked on 19 October 2026):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Engine instances per test run: 1 to 400 by default depending on the offer, 400 at most.&lt;/li&gt;
&lt;li&gt;Concurrent engine instances: 5 to 400 by default, up to 1,000 on request.&lt;/li&gt;
&lt;li&gt;Test duration: 3 to 24 hours by default, 24 hours at most.&lt;/li&gt;
&lt;li&gt;Fail criteria: up to 50 per test.&lt;/li&gt;
&lt;li&gt;Retention: server-side metrics 90 days, client-side metrics 365 days, test results and logs 6 months.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Raising a default limit means a support request; nothing goes past the stated maximums.&lt;/p&gt;
&lt;h2&gt;Can it test private endpoints and generate load from several regions?&lt;/h2&gt;
&lt;p&gt;Yes to both, with one catch: they do not combine. Multi-region load only works against public endpoints.&lt;/p&gt;
&lt;p&gt;By default the engines run in the region of your load testing resource. The &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/how-to-generate-load-from-multiple-regions&quot;&gt;multi-region guide&lt;/a&gt; lets you pick up to 5 regions per test and split the load between them, and it switches the test to public traffic when you do. For services without a public endpoint, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/how-to-test-private-endpoint&quot;&gt;virtual network injection&lt;/a&gt; places the engines inside the network that holds the application (checked on 19 October 2026).&lt;/p&gt;
&lt;h2&gt;What do the results and server metrics show?&lt;/h2&gt;
&lt;p&gt;The dashboard shows client-side metrics live during the run, such as virtual users, response time and requests per second. For Azure-hosted apps it adds server-side metrics from Azure Monitor, Application Insights and Container insights on the same timeline.&lt;/p&gt;
&lt;p&gt;That pairing is the service&amp;#39;s main advantage: you can see the database or container that saturates at the moment response times rise, without wiring up a separate dashboard. The &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/how-to-monitor-server-side-metrics&quot;&gt;server-side metrics guide&lt;/a&gt; lists the steps. Past runs stay comparable side by side, and you can download results for other tools.&lt;/p&gt;
&lt;h2&gt;How do pass/fail criteria and CI work?&lt;/h2&gt;
&lt;p&gt;You set fail criteria on client or server metrics, and the run is marked failed when any of them is breached. Pipelines in Azure DevOps, GitHub Actions or any CI tool that can call the Azure CLI can start a test and gate on that result.&lt;/p&gt;
&lt;p&gt;Per the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/how-to-define-test-criteria&quot;&gt;fail criteria guide&lt;/a&gt;, a criterion is an aggregate of a client metric, such as average response time or error percentage, compared with a threshold, either for the whole test or for one named request. Server metrics need the resource&amp;#39;s managed identity to read them. Auto-stop ends a run when the error rate passes a limit, so a wrong URL does not keep billing. The &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/load-testing/how-to-configure-load-test-cicd&quot;&gt;CI/CD guide&lt;/a&gt; covers GitHub Actions and Azure Pipelines (checked on 19 October 2026). Our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; explains how to choose the limits.&lt;/p&gt;
&lt;h2&gt;How much does Azure Load Testing cost?&lt;/h2&gt;
&lt;p&gt;You pay per virtual user hour (VUH): $0.15 per VUH for the first 10,000 VUH and $0.06 per VUH after that, the same in every region the pricing page lists. Since 1 March 2026 there is also a minimum charge per test run.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://azure.microsoft.com/en-us/pricing/details/load-testing/&quot;&gt;pricing page&lt;/a&gt; defines a VUH as virtual users multiplied by minutes, divided by 60, counted every 10 seconds; 1,000 users for 30 minutes is 500 VUH, or $75 at the first-tier rate. The minimum is 10 virtual users per engine for the run&amp;#39;s duration, and at least 10 minutes. A 5-user, 3-hour run on one engine is therefore billed as 30 VUH, not 15 (checked on 19 October 2026).&lt;/p&gt;
&lt;h2&gt;When should you choose something else?&lt;/h2&gt;
&lt;p&gt;Choose another option when your scripts, your cloud or your budget point elsewhere.&lt;/p&gt;
&lt;p&gt;Our overview of &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; covers the tools you can run yourself.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;LoadFocus&lt;/strong&gt;, which publishes this site, instead when you want to run &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=azure-load-testing&quot;&gt;JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions without an Azure subscription. It does not run Locust, and it runs k6 arrival-rate scenarios with a fixed number of users.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;Grafana Cloud k6&lt;/strong&gt; or the &lt;a href=&quot;https://grafana.com/docs/k6/latest/testing-guides/running-distributed-tests/&quot;&gt;k6-operator&lt;/a&gt; instead when your tests are written for k6, which Azure Load Testing cannot run (checked on 19 October 2026).&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;self-hosted Locust&lt;/strong&gt; instead when you already run Kubernetes or spare machines: its &lt;a href=&quot;https://docs.locust.io/en/stable/running-distributed.html&quot;&gt;distributed mode&lt;/a&gt; is free, and you only pay for the hardware.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Does Azure Load Testing support k6?&lt;/strong&gt;
No. It runs Apache JMeter and Locust tests, plus URL-based quick tests that it turns into a script for you. k6 scripts need another service or your own infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I use Azure Load Testing for an app that is not hosted on Azure?&lt;/strong&gt;
Yes. The test engines can target any public endpoint. Server-side metrics are only collected for Azure resources, so for other hosting you read server metrics from your own monitoring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is there a free tier?&lt;/strong&gt;
The pricing page lists only per-hour prices for load testing, with a minimum per test run. New Azure accounts get a $200 credit for their first 30 days, according to the same page.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust load testing: a review of the Python tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter: Python scripts or JMeter test plans?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;Load testing in GitHub Actions: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Apdex score: what it is and how to calculate it</title><link>https://loadtest.app/apdex-score/</link><guid isPermaLink="true">https://loadtest.app/apdex-score/</guid><description>Apdex score explained: the formula, how to choose the T threshold, a worked example, and how JMeter, Artillery and LoadFocus compute it in a load test.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;An Apdex score is a number between 0 and 1 that tells you what share of requests were fast enough for the people waiting on them. You choose a target time T: responses at or under T count in full, responses up to four times T count as half, and slower ones count as zero. A result of 1 means every request met the target, and 0 means every request was too slow.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Response time&lt;/th&gt;
&lt;th&gt;Weight in the score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Satisfied&lt;/td&gt;
&lt;td&gt;up to T&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tolerating&lt;/td&gt;
&lt;td&gt;above T, up to 4T&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frustrated&lt;/td&gt;
&lt;td&gt;above 4T&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The facts below come from the tools&amp;#39; own documentation and source code, checked on 19 October 2026.&lt;/p&gt;
&lt;h2&gt;How do you calculate an Apdex score?&lt;/h2&gt;
&lt;p&gt;Add the satisfied requests to half the tolerating ones and divide by all requests. The result always lands between 0 and 1.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Apdex(T) = (satisfied + tolerating / 2) / total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/apdex-measuring-user-satisfaction/&quot;&gt;New Relic&amp;#39;s Apdex documentation&lt;/a&gt; works through an example with T set to 500 ms: out of 200 requests, 170 finished within 500 ms, 20 took between 500 ms and 2 seconds, and 10 failed or took longer. That gives (170 + 20 / 2) / 200 = 0.90 (checked on 19 October 2026).&lt;/p&gt;
&lt;p&gt;A second example shows why the score is useful in a load test. Say a run sends 10,000 requests with T at 300 ms: 8,200 finish within 300 ms, 1,300 between 300 ms and 1.2 seconds, and 500 take longer. The Apdex is (8,200 + 650) / 10,000 = 0.885. The same run could easily report an average of 280 ms, which looks fine on its own and hides the 500 requests that took over a second.&lt;/p&gt;
&lt;h2&gt;Where does Apdex come from?&lt;/h2&gt;
&lt;p&gt;Apdex, short for Application Performance Index, is an open standard from a group of companies called the Apdex Alliance. Peter Sevcik of NetForecast founded the Alliance in 2004.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://en.wikipedia.org/wiki/Apdex&quot;&gt;Wikipedia article on Apdex&lt;/a&gt; cites Sevcik&amp;#39;s 2005 paper and gives the 4T rule. The &lt;a href=&quot;https://www.apdex.org/&quot;&gt;Apdex Users Group&lt;/a&gt; now says the rules for the T and F thresholds &amp;quot;have become flexible&amp;quot; (checked on 19 October 2026), and tools use that freedom.&lt;/p&gt;
&lt;h2&gt;How do JMeter, Artillery and LoadFocus compute it?&lt;/h2&gt;
&lt;p&gt;All three start from T = 500 ms. They differ on where the tolerating zone ends and on what they do with failed requests, so the same run can give three slightly different numbers.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Default T&lt;/th&gt;
&lt;th&gt;Tolerating up to&lt;/th&gt;
&lt;th&gt;Failed requests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;JMeter HTML dashboard&lt;/td&gt;
&lt;td&gt;500 ms&lt;/td&gt;
&lt;td&gt;1,500 ms&lt;/td&gt;
&lt;td&gt;counted as frustrated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artillery &lt;code&gt;apdex&lt;/code&gt; plugin&lt;/td&gt;
&lt;td&gt;500 ms&lt;/td&gt;
&lt;td&gt;4T (2,000 ms)&lt;/td&gt;
&lt;td&gt;only responses are scored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoadFocus result page&lt;/td&gt;
&lt;td&gt;500 ms, fixed&lt;/td&gt;
&lt;td&gt;4T (2,000 ms)&lt;/td&gt;
&lt;td&gt;scored on response time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Relic APM&lt;/td&gt;
&lt;td&gt;set per app&lt;/td&gt;
&lt;td&gt;4T&lt;/td&gt;
&lt;td&gt;server errors are frustrated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;JMeter.&lt;/strong&gt; The &lt;a href=&quot;https://jmeter.apache.org/usermanual/generating-dashboard.html&quot;&gt;dashboard report&lt;/a&gt; has an APDEX table per transaction. Its defaults are &lt;code&gt;jmeter.reportgenerator.apdex_satisfied_threshold=500&lt;/code&gt; and &lt;code&gt;apdex_tolerated_threshold=1500&lt;/code&gt;, so the tolerating zone ends at 3T unless you change it, and &lt;code&gt;apdex_per_transaction&lt;/code&gt; sets different thresholds per sampler. In &lt;a href=&quot;https://github.com/apache/jmeter/blob/master/src/core/src/main/java/org/apache/jmeter/report/processor/ApdexSummaryConsumer.java&quot;&gt;&lt;code&gt;ApdexSummaryConsumer&lt;/code&gt;&lt;/a&gt; a failed sample adds to the total and never to the satisfied or tolerating counts, so every failure pulls the score down (checked on 19 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Artillery.&lt;/strong&gt; The &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/apdex&quot;&gt;apdex plugin&lt;/a&gt; works with the HTTP engine only and reports &lt;code&gt;apdex.satisfied&lt;/code&gt;, &lt;code&gt;apdex.tolerated&lt;/code&gt; and &lt;code&gt;apdex.frustrated&lt;/code&gt; counters. Its source in &lt;a href=&quot;https://github.com/artilleryio/artillery/tree/main/packages/artillery-plugin-apdex&quot;&gt;artillery-plugin-apdex 1.24.0&lt;/a&gt; scores each response on its total time against T and 4T, then prints the score with a label: excellent from 0.94, good from 0.85, fair from 0.70, poor from 0.49, unacceptable below that. A request that never gets a response is never scored (checked on 19 October 2026). Our &lt;a href=&quot;https://loadtest.app/artillery-load-testing/&quot;&gt;Artillery review&lt;/a&gt; covers the tool itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LoadFocus&lt;/strong&gt;, which publishes this site, shows the Apdex score in the Insights tab of a load test result, next to a &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/response-time-distribution?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=apdex-score&quot;&gt;response-time histogram&lt;/a&gt;. T is fixed at 500 ms and the tolerating zone ends at 2,000 ms; you cannot change T. Each sample is classified by its response time, with the satisfied, tolerating and frustrated shares printed beside the score.&lt;/p&gt;
&lt;h2&gt;How do you choose the T threshold?&lt;/h2&gt;
&lt;p&gt;Pick T from what users of that page or endpoint expect, and set it per transaction when your tool allows it. A single T for a whole application blurs a fast search endpoint and a slow report export into one number.&lt;/p&gt;
&lt;p&gt;Some practical rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start from a real target.&lt;/strong&gt; If you already have a latency objective such as &amp;quot;95% of checkout calls under 400 ms&amp;quot;, T = 400 ms keeps the Apdex and the objective telling the same story.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep T the same between runs.&lt;/strong&gt; An Apdex is only comparable with another Apdex measured at the same T. Report it as Apdex(0.4) or &amp;quot;Apdex at T = 400 ms&amp;quot; so nobody compares across thresholds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Match the tolerating zone too.&lt;/strong&gt; JMeter&amp;#39;s default stops at 3T and the others at 4T. When you compare a JMeter report with another tool, set &lt;code&gt;apdex_tolerated_threshold&lt;/code&gt; to four times T first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not tune T to make a release pass.&lt;/strong&gt; Moving T after a bad run changes the ruler, and the trend you were watching is gone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is a good Apdex score?&lt;/h2&gt;
&lt;p&gt;Artillery&amp;#39;s plugin calls 0.94 and above excellent and 0.85 to 0.94 good, and anything under 0.70 poor or worse. Those labels only mean something if T was chosen honestly.&lt;/p&gt;
&lt;p&gt;An Apdex of 0.95 at T = 2 seconds can describe a slower service than 0.80 at T = 200 ms. When you set a release gate, write the threshold and the score together (&amp;quot;Apdex(0.5) of at least 0.9&amp;quot;) and check it next to a percentile limit, because the two catch different problems.&lt;/p&gt;
&lt;h2&gt;What does Apdex hide?&lt;/h2&gt;
&lt;p&gt;It squeezes the whole response-time distribution into three buckets, so it cannot tell you how slow the frustrated requests were. A request at 2.1 seconds and one at 30 seconds lower the score by exactly the same amount.&lt;/p&gt;
&lt;p&gt;That is why it works best next to other numbers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Percentiles&lt;/strong&gt; show how slow the slow requests are. Our guide to &lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;tail latency&lt;/a&gt; explains why the 99th percentile matters as soon as one page calls several services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error rate&lt;/strong&gt; catches failures your tool may not score. Artillery never scores requests that got no response, and New Relic counts server errors as frustrated, so read the error count separately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sample size&lt;/strong&gt; matters: New Relic notes that below about 100 requests per minute the score is unstable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measurement bias&lt;/strong&gt; applies too. A load generator that waits for slow responses sends fewer requests during slow periods, so the Apdex looks better than reality; see &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How do you use Apdex in a load test?&lt;/h2&gt;
&lt;p&gt;Treat it as a summary for people who will not read a histogram, and keep the pass/fail decision on explicit limits.&lt;/p&gt;
&lt;p&gt;A workable routine:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set T per key transaction from your latency objective.&lt;/li&gt;
&lt;li&gt;Run the test and record the Apdex at that T, along with the 95th and 99th percentiles and the error rate.&lt;/li&gt;
&lt;li&gt;Compare the Apdex with the previous run at the same T. A drop from 0.93 to 0.86 is a clear signal even to someone outside the team.&lt;/li&gt;
&lt;li&gt;Fail the build on percentile and error limits, as described in our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt;, and report the Apdex alongside them.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is a higher Apdex score better?&lt;/strong&gt;
Yes. 1.0 means every request finished within T, and 0 means every request took longer than the tolerating limit or was counted as frustrated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the default Apdex threshold in JMeter?&lt;/strong&gt;
The HTML dashboard uses 500 ms for satisfied and 1,500 ms for tolerated. Change them with &lt;code&gt;jmeter.reportgenerator.apdex_satisfied_threshold&lt;/code&gt; and &lt;code&gt;apdex_tolerated_threshold&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does Apdex include errors?&lt;/strong&gt;
It depends on the tool. JMeter counts failed samples as frustrated and New Relic treats server errors the same way, while Artillery&amp;#39;s plugin only scores requests that received a response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I compare Apdex scores from two tools?&lt;/strong&gt;
Only when T, the tolerating limit and the handling of errors match. Otherwise compare the underlying percentiles and error rates instead.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/artillery-load-testing/&quot;&gt;Artillery load testing: a review of the Node.js tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/slo-vs-sla/&quot;&gt;SLO vs SLA: the difference, SLIs and error budgets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Artillery load testing: a review of the Node.js tool</title><link>https://loadtest.app/artillery-load-testing/</link><guid isPermaLink="true">https://loadtest.app/artillery-load-testing/</guid><description>Artillery load testing reviewed: YAML and TypeScript scripts, arrival-rate phases, Playwright tests, ensure checks, AWS and Azure runs and pricing.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Artillery is an open source load testing tool that runs on Node.js and describes each test as a YAML, TypeScript or JavaScript file. Artillery load testing suits teams that live in the JavaScript ecosystem, want new users to arrive at a set rate, or want browser tests with Playwright in the same tool. Teams that want a single binary with no runtime are better served by k6, and teams that need many protocols out of the box by JMeter.&lt;/p&gt;
&lt;p&gt;We have not run Artillery for this review. The facts come from Artillery&amp;#39;s documentation, repository, npm package and pricing page, checked on 19 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Artillery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;MPL-2.0; some Azure modules under BSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Node.js, latest LTS (package needs 22.18.0 or later)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current release&lt;/td&gt;
&lt;td&gt;2.0.34 (14 August 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written as&lt;/td&gt;
&lt;td&gt;YAML, TypeScript or JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;New virtual users per second, in phases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in engines&lt;/td&gt;
&lt;td&gt;HTTP, WebSocket, Socket.IO, Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass/fail checks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ensure&lt;/code&gt; plugin, non-zero exit code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run-lambda&lt;/code&gt;, &lt;code&gt;run-fargate&lt;/code&gt;, &lt;code&gt;run-aci&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted reports&lt;/td&gt;
&lt;td&gt;Artillery Cloud, opt-in, free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Who makes Artillery, and how is it licensed?&lt;/h2&gt;
&lt;p&gt;Artillery Software Inc. maintains it on GitHub. Most of the code is MPL-2.0, which allows commercial use; some Azure-specific modules are under the Business Source License.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/artilleryio/artillery&quot;&gt;repository README&lt;/a&gt; says you may use Artillery on Azure for evaluation and proof-of-concept work, and that commercial or production use there needs a commercial licence. The latest release is &lt;a href=&quot;https://github.com/artilleryio/artillery/releases/tag/artillery-2.0.34&quot;&gt;2.0.34&lt;/a&gt;, published to npm on 14 August 2026, and its package metadata asks for Node.js 22.18.0 or later; the &lt;a href=&quot;https://www.artillery.io/docs/get-started/get-artillery&quot;&gt;install guide&lt;/a&gt; says to use the most recent LTS release (checked on 19 October 2026).&lt;/p&gt;
&lt;h2&gt;How does an Artillery test script work?&lt;/h2&gt;
&lt;p&gt;A script has two parts: &lt;code&gt;config&lt;/code&gt;, which sets the target, the load phases and plugins, and &lt;code&gt;scenarios&lt;/code&gt;, which list what each virtual user does. You can write it in YAML, or export &lt;code&gt;config&lt;/code&gt; and &lt;code&gt;scenarios&lt;/code&gt; from a TypeScript or JavaScript file.&lt;/p&gt;
&lt;p&gt;This test ramps from 5 to 50 new users per second over two minutes, holds 50 per second for ten minutes, and fails if the 99th percentile response time reaches 800 ms:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;config:
  target: https://staging.example.com
  phases:
    - duration: 120
      arrivalRate: 5
      rampTo: 50
      name: ramp
    - duration: 600
      arrivalRate: 50
      name: sustain
  plugins:
    ensure:
      thresholds:
        - http.response_time.p99: 800
scenarios:
  - name: browse and add to cart
    flow:
      - get:
          url: /products
      - think: 2
      - post:
          url: /cart
          json:
            sku: A-100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Per the &lt;a href=&quot;https://www.artillery.io/docs/reference/test-script&quot;&gt;test script reference&lt;/a&gt;, the four phase types are a constant arrival rate, a linear ramp with &lt;code&gt;rampTo&lt;/code&gt;, a fixed &lt;code&gt;arrivalCount&lt;/code&gt; spread over a period, and a &lt;code&gt;pause&lt;/code&gt;. Custom code loaded through &lt;code&gt;config.processor&lt;/code&gt; can be a TypeScript module locally and on Fargate, but not on Lambda (checked on 19 October 2026).&lt;/p&gt;
&lt;h2&gt;What load model does Artillery use?&lt;/h2&gt;
&lt;p&gt;Artillery starts new virtual users at a rate you set, whether or not earlier users have finished. That is an open workload model, which keeps arrivals steady even when the system slows down.&lt;/p&gt;
&lt;p&gt;Each new user picks one scenario, runs it and leaves, so a slow system collects users the way real traffic would. &lt;code&gt;maxVusers&lt;/code&gt; caps the number of concurrent users in a phase if you need to protect the load generator. Our article on the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains the difference, and &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law&lt;/a&gt; shows how arrival rate and response time set the number of concurrent users.&lt;/p&gt;
&lt;h2&gt;Which protocols and browsers can it test?&lt;/h2&gt;
&lt;p&gt;HTTP is the default, and WebSocket, Socket.IO and Playwright come built in. gRPC and AWS Kinesis need separately installed engines.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.artillery.io/docs/reference/engines&quot;&gt;engines page&lt;/a&gt; describes the model: each scenario runs on one engine, and you can write your own. On npm, &lt;code&gt;artillery-engine-grpc&lt;/code&gt; is at 0.0.9 and &lt;code&gt;artillery-engine-kinesis&lt;/code&gt; at 1.1.1, both MPL-2.0. The &lt;a href=&quot;https://www.artillery.io/docs/reference/engines/playwright&quot;&gt;Playwright engine&lt;/a&gt; runs real headless browsers from your Playwright code and reports Web Vitals such as TTFB, FCP and LCP per page. The docs warn that a separate browser per user, instead of a browser context, needs a lot more CPU and memory (checked on 19 October 2026). Our roundup of &lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools&lt;/a&gt; puts Artillery next to other options for real-time apps.&lt;/p&gt;
&lt;h2&gt;How does it fit into CI?&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;ensure&lt;/code&gt; plugin turns metrics into pass/fail checks, and a failed check makes Artillery exit with a non-zero code. That is enough for any CI system to fail the build.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/ensure&quot;&gt;ensure reference&lt;/a&gt; offers two kinds of check: &lt;code&gt;thresholds&lt;/code&gt;, where a metric must stay below a value, and &lt;code&gt;conditions&lt;/code&gt;, which combine metrics in an expression such as &lt;code&gt;http.response_time.p95 &amp;lt; 250 and http.request_rate &amp;gt; 1000&lt;/code&gt;. A condition marked &lt;code&gt;strict: false&lt;/code&gt; reports without failing. The &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/apdex&quot;&gt;apdex plugin&lt;/a&gt; adds an Apdex score for HTTP tests, with a default threshold of 500 ms (checked on 19 October 2026); our article on the &lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score&lt;/a&gt; explains how to read it. For a workflow file, see our guide to &lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;load testing in GitHub Actions&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How do you scale out an Artillery test?&lt;/h2&gt;
&lt;p&gt;From the command line, on your own cloud account: &lt;code&gt;run-lambda&lt;/code&gt; and &lt;code&gt;run-fargate&lt;/code&gt; on AWS, &lt;code&gt;run-aci&lt;/code&gt; on Azure Container Instances. Artillery creates the resources it needs for the run and removes the temporary ones afterwards.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.artillery.io/docs/load-testing-at-scale/aws-fargate&quot;&gt;Fargate guide&lt;/a&gt; lists the resources and IAM permissions involved. The &lt;a href=&quot;https://www.artillery.io/docs/load-testing-at-scale/aws-lambda&quot;&gt;Lambda guide&lt;/a&gt; states its limits: a test cannot run longer than 15 minutes, a running test cannot be stopped, and &lt;code&gt;before&lt;/code&gt; and &lt;code&gt;after&lt;/code&gt; hooks run once per worker. The &lt;a href=&quot;https://www.artillery.io/docs/load-testing-at-scale/azure-aci&quot;&gt;Azure guide&lt;/a&gt; needs Artillery 2.0.19 or later and, outside evaluation, a subscription (checked on 19 October 2026).&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run Artillery scripts. It &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=artillery-load-testing&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions, so for Artillery the commands above are the way to scale.&lt;/p&gt;
&lt;h2&gt;What does Artillery Cloud cost?&lt;/h2&gt;
&lt;p&gt;The dashboard is optional: results go to it only when you run with &lt;code&gt;--record&lt;/code&gt; and an API key. Plans run from a free tier to $499 a month.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.artillery.io/pricing&quot;&gt;pricing page&lt;/a&gt; lists (checked on 19 October 2026):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free:&lt;/strong&gt; 30 reports a month, 1 month of retention, 2 team members, distributed tests up to 5 workers and 30 minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team, $199 a month:&lt;/strong&gt; 1,000 reports, 6 months, 8 members, up to 25 workers and 2 hours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business, $499 a month:&lt;/strong&gt; 2,500 reports, 18 months, no limit on members, workers or duration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Enterprise SSO, audit logs and support SLAs start at $1,199 a month, and annual billing saves 20%.&lt;/p&gt;
&lt;h2&gt;What does Artillery do well, and where are its limits?&lt;/h2&gt;
&lt;p&gt;It is strong on arrival-rate load, short readable scripts and browser tests with Playwright. It is weaker where you need a single binary, many protocols without add-ons, or Azure use without a licence.&lt;/p&gt;
&lt;p&gt;As a Node.js tool it can use any npm module in custom functions, at the cost of one more runtime on every test machine. Limits the docs state themselves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lambda runs stop at 15 minutes and cannot be cancelled.&lt;/li&gt;
&lt;li&gt;TypeScript custom code (&lt;code&gt;config.processor&lt;/code&gt;) does not run on Lambda.&lt;/li&gt;
&lt;li&gt;gRPC and Kinesis are separate engines.&lt;/li&gt;
&lt;li&gt;Production use on Azure requires a commercial licence.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When should you choose k6, JMeter or Locust instead?&lt;/h2&gt;
&lt;p&gt;Choose another tool when its runtime, protocols or language fit your team better.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;k6&lt;/strong&gt; instead when you want one Go binary with no Node.js to manage and thresholds in the script. Our &lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery&lt;/a&gt; comparison covers the two side by side. LoadFocus &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=artillery-load-testing&quot;&gt;runs k6 scripts&lt;/a&gt; from 25 cloud regions, though it runs arrival-rate scenarios with a fixed number of users, so for a held arrival rate run them with k6 itself.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;JMeter&lt;/strong&gt; instead when you need JDBC, JMS, LDAP or FTP out of the box, or testers who build tests in a GUI.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;Locust&lt;/strong&gt; instead when your team writes Python; our &lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust review&lt;/a&gt; covers it. The &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; overview sets all of them side by side.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Artillery free for commercial use?&lt;/strong&gt;
Most of the CLI is MPL-2.0, which permits commercial use. The exception is Azure: commercial or production use of its Azure modules needs a commercial licence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Artillery run browser tests?&lt;/strong&gt;
Yes. Its built-in Playwright engine runs your Playwright code in headless browsers and reports Web Vitals for each page.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Artillery scripts?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To scale an Artillery test, use &lt;code&gt;run-lambda&lt;/code&gt;, &lt;code&gt;run-fargate&lt;/code&gt; or &lt;code&gt;run-aci&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools: six options compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score: what it is and how to calculate it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Tail latency: what it is and how to measure it</title><link>https://loadtest.app/tail-latency/</link><guid isPermaLink="true">https://loadtest.app/tail-latency/</guid><description>Tail latency explained: why the slowest 1% of requests matters, how fan-out makes it worse, what causes it, and how to measure p99 honestly in a load test.</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Tail latency is the response time of the slowest few requests, usually reported as the 99th or 99.9th percentile. It matters because one user action often waits on many requests, so a delay that hits 1 request in 100 can reach most of your users. A load test that reports only the average or median will not show it.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend calls per page&lt;/th&gt;
&lt;th&gt;Share of pages hitting at least one 1-in-100 slow call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;18.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;39.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;63.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table assumes each call is slow with a 1% chance, independently of the others: the share is &lt;code&gt;1 - 0.99^n&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;What counts as the tail?&lt;/h2&gt;
&lt;p&gt;The tail is the part of the response-time distribution above a high percentile, most often p99 and p99.9. The median describes a typical request, and the tail describes the requests people complain about.&lt;/p&gt;
&lt;p&gt;A percentile is a cut-off: p99 = 800 ms means 99% of requests took 800 ms or less and 1% took longer. The tail sits to the right of that cut-off on a response-time histogram, often as a long, thin stretch far from the main peak. Our guide to &lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;throughput vs latency&lt;/a&gt; explains how latency and percentiles are measured in load testing tools.&lt;/p&gt;
&lt;h2&gt;Why does tail latency matter so much?&lt;/h2&gt;
&lt;p&gt;Because requests multiply. A page or API call that fans out to many services finishes only when the slowest of them answers, so the tail of each service becomes the typical experience of the whole.&lt;/p&gt;
&lt;p&gt;The standard reference is &amp;quot;The Tail at Scale&amp;quot; by Jeffrey Dean and Luiz André Barroso, published in Communications of the ACM in 2013 (&lt;a href=&quot;https://www.barroso.org/publications/TheTailAtScale.pdf&quot;&gt;paper PDF&lt;/a&gt;, checked on 19 October 2026). Their example: a server that usually answers in 10 ms but takes one second at the 99th percentile. If one request touches one such server, 1 user request in 100 is slow. If it must collect answers from 100 of them in parallel, 63% of user requests take more than one second. Even with only 1 slow call in 10,000, a service with 2,000 servers sees almost 1 in 5 requests over a second.&lt;/p&gt;
&lt;p&gt;They also report measurements from a real Google service: the 99th percentile for a single leaf request was 10 ms, but for all leaf requests to finish it was 140 ms, and waiting for the slowest 5% of them accounted for half of that.&lt;/p&gt;
&lt;h2&gt;What causes tail latency?&lt;/h2&gt;
&lt;p&gt;Usually interference from other work on the machine or in the network. The same request is fast most of the time and slow when something else is going on.&lt;/p&gt;
&lt;p&gt;Dean and Barroso list the usual suspects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shared resources:&lt;/strong&gt; other work on the same machine competing for CPU cores, processor caches, memory bandwidth or network bandwidth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background daemons&lt;/strong&gt; that wake up and take a slice of the machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintenance work&lt;/strong&gt; such as log compaction or garbage collection in managed runtimes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queueing&lt;/strong&gt; in servers and network switches, which amplifies every other source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Power and thermal limits&lt;/strong&gt; that slow a CPU after a burst of work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In load tests, the same list shows up as GC pauses in JVM or Node.js services, connection pool exhaustion, a slow database query that only some requests trigger, cold caches after a deploy, and retries after a timeout. A tail that grows with load points at queueing; a tail that is there even at low load points at something periodic, such as GC or a scheduled job.&lt;/p&gt;
&lt;h2&gt;How do you measure tail latency in a load test?&lt;/h2&gt;
&lt;p&gt;Collect enough samples, ask your tool for the right percentiles, and never average percentiles across machines or time windows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enough samples.&lt;/strong&gt; p99 is decided by the slowest 1% of requests, so a run with 500 requests puts it on 5 of them. For a stable p99 you want thousands of requests per transaction, and for p99.9 tens of thousands. Short smoke tests are fine for catching errors and poor for judging the tail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The right percentiles.&lt;/strong&gt; Defaults often stop short of the tail. k6&amp;#39;s &lt;code&gt;summaryTrendStats&lt;/code&gt; default is &lt;code&gt;avg,min,med,max,p(90),p(95)&lt;/code&gt;, so p99 only appears if you add it (&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/k6-options/reference/&quot;&gt;k6 options reference&lt;/a&gt;, checked on 19 October 2026):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;k6 run --summary-trend-stats=&amp;quot;med,p(95),p(99),p(99.9),max&amp;quot; \
  script.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Aggregate_Report&quot;&gt;Aggregate Report&lt;/a&gt; shows the 90%, 95% and 99% lines, and the &lt;code&gt;aggregate_rpt_pct1&lt;/code&gt; to &lt;code&gt;aggregate_rpt_pct3&lt;/code&gt; properties change which percentiles appear (checked on 19 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No averaged percentiles.&lt;/strong&gt; The p99 of a whole run is not the average of each minute&amp;#39;s p99, and it is not the average of each load generator&amp;#39;s p99 either. Merge the raw samples or a histogram, then compute the percentile once. The &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt; article covers HdrHistogram, the usual tool for that, and a second trap: a load generator that waits for slow responses sends fewer requests while the system is slow, which hides the tail you are looking for.&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, shows P95 and P99 tiles for each run, a &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/response-time-distribution?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tail-latency&quot;&gt;response-time histogram&lt;/a&gt; built from the individual samples, and the slowest requests, so a long tail to the right of the peak is visible at a glance.&lt;/p&gt;
&lt;h2&gt;How do you set a pass/fail limit on the tail?&lt;/h2&gt;
&lt;p&gt;Put a limit on p99 per key transaction, next to a p95 limit and an error-rate limit. Base it on what users of that transaction need, and keep the run long enough for p99 to be stable.&lt;/p&gt;
&lt;p&gt;A sensible set for a checkout API could read: p95 under 400 ms, p99 under 1,200 ms, errors under 0.5%. In k6 those are thresholds on &lt;code&gt;http_req_duration&lt;/code&gt; such as &lt;code&gt;p(99)&amp;lt;1200&lt;/code&gt;, and a failed threshold makes k6 exit with a non-zero code (&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;k6 thresholds&lt;/a&gt;, checked on 19 October 2026). LoadFocus has a &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/pass-fail-thresholds?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tail-latency&quot;&gt;Max P99 response time&lt;/a&gt; field next to Max P95 for cloud, JMeter and k6 tests, and every run is then graded PASS or FAIL. Our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; covers the rest of the set.&lt;/p&gt;
&lt;p&gt;If you also report an &lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score&lt;/a&gt;, remember it puts every request over four times T in one bucket, so it cannot tell a 2-second tail from a 30-second one. The p99 can.&lt;/p&gt;
&lt;h2&gt;How do you reduce tail latency?&lt;/h2&gt;
&lt;p&gt;Remove the sources of variability where you can, and design callers so one slow call does not hold up the whole response.&lt;/p&gt;
&lt;p&gt;Common fixes, roughly from cheapest to most involved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tune GC and pool sizes&lt;/strong&gt; so pauses and waits for a free connection stay short under load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep headroom.&lt;/strong&gt; Queueing grows sharply near saturation, so spare capacity shortens the tail; the &lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;throughput vs latency&lt;/a&gt; guide shows how to find the knee.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set timeouts and limit retries&lt;/strong&gt; so a stuck call fails fast instead of piling up behind it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use hedged requests&lt;/strong&gt; for idempotent reads: send a second copy to another replica if the first has not answered in time. Dean and Barroso describe sending the backup only after the 95th-percentile expected latency, which limits the extra load to about 5%. In one of their tests, a hedge after 10 ms cut the 99.9th percentile from 1,800 ms to 74 ms while sending just 2% more requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fan out less.&lt;/strong&gt; Fewer backend calls per page means fewer chances to hit the tail, as the table at the top shows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After each change, run the same test again with the same load profile and compare p99 and p99.9 between runs.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is tail latency the same as p99?&lt;/strong&gt;
p99 is the most common way to report it, and the tail is the whole slow end of the distribution. Track p99.9 and the maximum as well when the run has enough requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why is my p99 so much higher than my average?&lt;/strong&gt;
Because a few slow requests barely move the average and fully define the p99. A large gap is normal; a gap that grows as load rises usually means queueing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many requests do I need to measure p99?&lt;/strong&gt;
At the very least a few thousand per transaction, so that the p99 rests on dozens of slow requests. For p99.9, plan for tens of thousands.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score: what it is and how to calculate it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/ycsb/&quot;&gt;YCSB: workloads A-F, how to run it and its limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/slo-vs-sla/&quot;&gt;SLO vs SLA: the difference, SLIs and error budgets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>gRPC load testing: tools, setup and what to watch</title><link>https://loadtest.app/grpc-load-testing/</link><guid isPermaLink="true">https://loadtest.app/grpc-load-testing/</guid><description>gRPC load testing explained: which tools speak gRPC natively, how to write a k6 or ghz test, and why failed calls still come back as HTTP 200 responses.</description><pubDate>Fri, 16 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;gRPC load testing needs a tool that speaks HTTP/2 and Protocol Buffers: k6 and ghz do it out of the box, JMeter needs a plugin, Gatling&amp;#39;s gRPC support is capped in its free edition, and Locust needs you to wrap a gRPC client yourself. Count results by gRPC status, not HTTP status, because failed calls still arrive as HTTP 200, and check that the load reaches every backend.&lt;/p&gt;
&lt;p&gt;No gRPC test was run for this article. Tool facts come from each project&amp;#39;s documentation and repository, checked on 16 October 2026; LoadFocus facts come from its code.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;gRPC support&lt;/th&gt;
&lt;th&gt;Loads the API from&lt;/th&gt;
&lt;th&gt;Rate control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;Built in (&lt;code&gt;k6/net/grpc&lt;/code&gt;), unary calls and streams&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.proto&lt;/code&gt; files or server reflection&lt;/td&gt;
&lt;td&gt;Arrival-rate executors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ghz&lt;/td&gt;
&lt;td&gt;Purpose-built gRPC CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.proto&lt;/code&gt;, protoset or server reflection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--rps&lt;/code&gt; and load schedules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMeter&lt;/td&gt;
&lt;td&gt;Third-party plugin (jmeter-grpc-request)&lt;/td&gt;
&lt;td&gt;A folder of &lt;code&gt;.proto&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;Thread groups and timers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;Separate gRPC component; free edition capped at 5 users, 5 minutes&lt;/td&gt;
&lt;td&gt;Code generated from &lt;code&gt;.proto&lt;/code&gt; with protoc&lt;/td&gt;
&lt;td&gt;Injection profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;No built-in client; wrap one and report calls&lt;/td&gt;
&lt;td&gt;Your Python gRPC client&lt;/td&gt;
&lt;td&gt;Wait times, &lt;code&gt;constant_throughput&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Why does gRPC need a different load test from REST?&lt;/h2&gt;
&lt;p&gt;gRPC runs over HTTP/2, sends binary Protocol Buffers instead of JSON, puts the call&amp;#39;s result in a trailer rather than the HTTP status, and multiplexes many calls on one connection. Each of those changes how you script the test or read its results.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://grpc.io/blog/grpc-load-balancing/&quot;&gt;gRPC load balancing post&lt;/a&gt; lists the HTTP/2 traits that matter here: a binary protocol, many requests multiplexed on one connection and header compression, with strongly typed Protobuf messages on top (checked on 16 October 2026). A tool that only sends JSON over HTTP/1.1 cannot call a gRPC method, so check protocol support first.&lt;/p&gt;
&lt;h2&gt;Which tool should you use?&lt;/h2&gt;
&lt;p&gt;Use k6 if you want scripted scenarios, checks and thresholds in code; use ghz for a quick benchmark of one method from the command line. Reach for JMeter, Gatling or Locust when your team already runs everything else in them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;k6.&lt;/strong&gt; The &lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6-net-grpc/&quot;&gt;k6/net/grpc module&lt;/a&gt; loads Protobuf definitions, connects, makes unary calls with &lt;code&gt;invoke&lt;/code&gt; and opens streams. It reports &lt;code&gt;grpc_req_duration&lt;/code&gt; and stream message counts in its &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/metrics/reference/&quot;&gt;metrics&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ghz.&lt;/strong&gt; A gRPC-only benchmarking CLI. Its &lt;a href=&quot;https://ghz.sh/docs/options&quot;&gt;options&lt;/a&gt; include &lt;code&gt;--rps&lt;/code&gt;, &lt;code&gt;--concurrency&lt;/code&gt;, &lt;code&gt;--total&lt;/code&gt; and &lt;code&gt;--duration&lt;/code&gt;, and if you pass neither &lt;code&gt;--proto&lt;/code&gt; nor &lt;code&gt;--protoset&lt;/code&gt; it tries server reflection. The latest &lt;a href=&quot;https://github.com/bojand/ghz/releases&quot;&gt;release&lt;/a&gt; is v0.121.0 (checked on 16 October 2026).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JMeter.&lt;/strong&gt; Core JMeter has no gRPC sampler; the &lt;a href=&quot;https://github.com/zalopay-oss/jmeter-grpc-request&quot;&gt;jmeter-grpc-request plugin&lt;/a&gt; adds one that reads a folder of &lt;code&gt;.proto&lt;/code&gt; files, with no code generation (latest tag v1.2.6, checked on 16 October 2026).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gatling.&lt;/strong&gt; The &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;Gatling README&lt;/a&gt; puts gRPC, MQTT and GraphQL under the Gatling Enterprise Component License, capped at 5 concurrent users and 5-minute tests in Community Edition (checked on 16 October 2026). Its &lt;a href=&quot;https://docs.gatling.io/reference/script/grpc/setup/&quot;&gt;gRPC setup guide&lt;/a&gt; generates code from your &lt;code&gt;.proto&lt;/code&gt; files with protoc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Locust.&lt;/strong&gt; Its docs on &lt;a href=&quot;https://docs.locust.io/en/stable/testing-other-systems.html&quot;&gt;testing other systems&lt;/a&gt; include a gRPC example: you wrap a gRPC client so each call is reported to Locust, and &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;&lt;code&gt;constant_throughput&lt;/code&gt;&lt;/a&gt; paces a task to at most a set number of runs per second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose ghz instead of k6 when you only need to know how one method behaves at a given rate and do not want to write a script. Choose Locust instead when your team writes Python and the test needs logic that is easier there.&lt;/p&gt;
&lt;h2&gt;How do you write a gRPC load test in k6?&lt;/h2&gt;
&lt;p&gt;Load the &lt;code&gt;.proto&lt;/code&gt; file in the init code, then connect, invoke the method and check the gRPC status in each iteration. The structure follows the k6 documentation&amp;#39;s own example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import grpc from &amp;#39;k6/net/grpc&amp;#39;;
import { check } from &amp;#39;k6&amp;#39;;

const client = new grpc.Client();
client.load([&amp;#39;protos&amp;#39;], &amp;#39;hello.proto&amp;#39;);

export const options = { vus: 20, duration: &amp;#39;1m&amp;#39; };

export default () =&amp;gt; {
  client.connect(&amp;#39;grpc.example.com:443&amp;#39;);
  const res = client.invoke(&amp;#39;hello.Greeter/SayHello&amp;#39;, {
    name: &amp;#39;load test&amp;#39;,
  });
  check(res, {
    &amp;#39;status is OK&amp;#39;: (r) =&amp;gt; r &amp;amp;&amp;amp; r.status === grpc.StatusOK,
  });
  client.close();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6-net-grpc/client/client-invoke/&quot;&gt;&lt;code&gt;invoke&lt;/code&gt; page&lt;/a&gt; uses the same load, connect, invoke, check and close sequence. &lt;code&gt;connect&lt;/code&gt; uses TLS unless you pass &lt;code&gt;plaintext: true&lt;/code&gt;, and with &lt;code&gt;reflect: true&lt;/code&gt; it asks the server for its API instead of reading a &lt;code&gt;.proto&lt;/code&gt; file (&lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6-net-grpc/client/client-connect/&quot;&gt;connect parameters&lt;/a&gt;). To hold a request rate rather than a number of users, run the function in an arrival-rate scenario.&lt;/p&gt;
&lt;h2&gt;How do you run a quick benchmark with ghz?&lt;/h2&gt;
&lt;p&gt;Point ghz at the method, give it a request body and a rate or concurrency, and it prints a latency histogram, percentiles and a count for each status code. It suits a first look at one method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ghz --insecure --proto ./hello.proto \
  --call hello.Greeter.SayHello \
  -d &amp;#39;{&amp;quot;name&amp;quot;:&amp;quot;load test&amp;quot;}&amp;#39; \
  --rps 200 -c 50 -z 1m \
  localhost:50051
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--rps&lt;/code&gt; sets the target request rate, shared among the workers, &lt;code&gt;-c&lt;/code&gt; sets how many workers there are, and &lt;code&gt;-z&lt;/code&gt; sets the duration. Drop &lt;code&gt;--proto&lt;/code&gt; to let ghz use server reflection, if the server enables it. The ghz &lt;a href=&quot;https://ghz.sh/docs/output&quot;&gt;output page&lt;/a&gt; shows a sample report.&lt;/p&gt;
&lt;h2&gt;What should you measure in a gRPC load test?&lt;/h2&gt;
&lt;p&gt;Measure call latency percentiles, throughput and the count of each gRPC status code. An HTTP-level success rate can read 100% while calls fail, because the result travels in the &lt;code&gt;grpc-status&lt;/code&gt; trailer.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://raw.githubusercontent.com/grpc/grpc/master/doc/PROTOCOL-HTTP2.md&quot;&gt;gRPC HTTP/2 protocol spec&lt;/a&gt; sets the HTTP status to &lt;code&gt;:status 200&lt;/code&gt; and carries the call&amp;#39;s result in trailers as &lt;code&gt;grpc-status&lt;/code&gt; and &lt;code&gt;grpc-message&lt;/code&gt;. The &lt;a href=&quot;https://grpc.github.io/grpc/core/md_doc_statuscodes.html&quot;&gt;status code list&lt;/a&gt; has the codes that matter most under load:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DEADLINE_EXCEEDED&lt;/code&gt; (4): the deadline expired before the call completed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RESOURCE_EXHAUSTED&lt;/code&gt; (8): a quota or resource ran out.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INTERNAL&lt;/code&gt; (13): an internal invariant broke, a code reserved for serious errors.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; (14): usually transient; retrying with backoff can clear it, though not always safely for non-idempotent calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Count each one per load step. Deadline errors can climb while the calls that succeed still look fast, because the slow calls are cut off at the deadline instead of being recorded as slow. Our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; shows how to turn those counts into thresholds.&lt;/p&gt;
&lt;h2&gt;Why does all the load land on one server?&lt;/h2&gt;
&lt;p&gt;Because gRPC multiplexes many calls on one long-lived connection, a load balancer that works per connection sends every call on it to the same backend. A load generator with few connections can then saturate one server while the others sit idle.&lt;/p&gt;
&lt;p&gt;The gRPC load balancing post explains the mechanism. A transport-level (L3/L4) balancer terminates the TCP connection and opens another to one chosen backend, copying the HTTP/2 and gRPC frames between them; an application-level (L7) balancer parses HTTP/2 and can spread the streams of one client connection across backends. If your production clients go through an L7 balancer or balance on the client side, test through the same path, and check per-backend CPU and request counts during the run.&lt;/p&gt;
&lt;h2&gt;Can you run gRPC load tests on LoadFocus?&lt;/h2&gt;
&lt;p&gt;Not in a way its reports would show. LoadFocus runs the stock k6 v0.57.0 release, which includes the gRPC module, but its k6 agent forwards only HTTP request samples to the results, so gRPC calls would generate load without appearing in its charts or error counts.&lt;/p&gt;
&lt;p&gt;Two more limits apply. The k6 upload accepts &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.csv&lt;/code&gt;, &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.properties&lt;/code&gt; files, not &lt;code&gt;.proto&lt;/code&gt; files, so a script would have to connect with &lt;code&gt;reflect: true&lt;/code&gt;. And the JMeter route is closed: LoadFocus&amp;#39;s JMeter agents ship JMeter 5.6.2 with plugins that do not include a gRPC sampler, and the JMeter upload does not accept plugin JAR files. For gRPC, run k6 or ghz on your own machines, and use LoadFocus&amp;#39;s &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=grpc-load-testing&quot;&gt;k6 scripts&lt;/a&gt; for the HTTP side of the same system, where it generates load from 25 regions.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Can you load test gRPC streaming calls?&lt;/strong&gt;
Yes, with k6 and ghz. The k6 gRPC module has a &lt;code&gt;Stream&lt;/code&gt; class with &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt; and event handlers, and ghz has options for streaming intervals and durations. Measure messages sent and received as well as call latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do you need the .proto file to load test a gRPC service?&lt;/strong&gt;
Not if the server enables reflection. k6 can connect with &lt;code&gt;reflect: true&lt;/code&gt;, and ghz falls back to reflection when you pass neither &lt;code&gt;--proto&lt;/code&gt; nor &lt;code&gt;--protoset&lt;/code&gt;. Without reflection you need the &lt;code&gt;.proto&lt;/code&gt; file or a compiled protoset.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do my gRPC errors not show up as failed requests?&lt;/strong&gt;
Because the HTTP status of a gRPC call is 200 even when the call fails. The result is in the &lt;code&gt;grpc-status&lt;/code&gt; trailer, so check that value in your script and count each code separately.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools: six options compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-load-test-a-graphql-api/&quot;&gt;How to load test a GraphQL API: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6: JVM simulations or a single Go binary?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools: wrk, hey, oha, ab and more&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Throughput vs latency: how they interact under load</title><link>https://loadtest.app/throughput-vs-latency/</link><guid isPermaLink="true">https://loadtest.app/throughput-vs-latency/</guid><description>Throughput vs latency explained: what each measures, why JMeter&apos;s Latency is not response time, and why latency climbs once throughput hits a ceiling.</description><pubDate>Fri, 16 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Throughput is how much work a system finishes per unit of time, such as requests per second. Latency is how long one piece of work takes, such as 120 milliseconds for one request. Under light load the two barely affect each other; as load approaches the system&amp;#39;s capacity, throughput stops rising and latency starts to climb, and finding that point is what a load test is for.&lt;/p&gt;
&lt;p&gt;The worked example below is arithmetic with no test run behind it. Tool definitions come from each project&amp;#39;s documentation, checked on 16 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Question it answers&lt;/td&gt;
&lt;td&gt;How much finishes per second?&lt;/td&gt;
&lt;td&gt;How long does one take?&lt;/td&gt;
&lt;td&gt;How much could the link carry?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical unit&lt;/td&gt;
&lt;td&gt;requests or transactions per second&lt;/td&gt;
&lt;td&gt;milliseconds&lt;/td&gt;
&lt;td&gt;megabits per second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In k6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http_reqs&lt;/code&gt; (count and rate)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http_req_duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not a test target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In JMeter&lt;/td&gt;
&lt;td&gt;Throughput&lt;/td&gt;
&lt;td&gt;Elapsed time (and a separate &amp;quot;Latency&amp;quot;)&lt;/td&gt;
&lt;td&gt;not a test target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between throughput and latency?&lt;/h2&gt;
&lt;p&gt;Throughput counts completed work over time; latency measures the time one unit of work takes. A system can have low latency and low throughput, or high throughput and high latency, so you need both numbers to describe it.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/glossary.html&quot;&gt;glossary&lt;/a&gt; calculates throughput as the number of requests divided by the total time, from the start of the first sample to the end of the last, including the gaps between samples, &amp;quot;as it is supposed to represent the load on the server&amp;quot; (checked on 16 October 2026). That makes throughput a property of the whole run, while latency belongs to each request and is best read as a distribution: median, p95, p99.&lt;/p&gt;
&lt;h2&gt;Is bandwidth the same as throughput?&lt;/h2&gt;
&lt;p&gt;No. Bandwidth is the most data a network link could carry; throughput is what actually gets through. In a load test, throughput usually means completed requests, and bandwidth matters only when large payloads fill the link before the application runs out of capacity.&lt;/p&gt;
&lt;p&gt;AWS&amp;#39;s &lt;a href=&quot;https://aws.amazon.com/compare/the-difference-between-throughput-and-latency/&quot;&gt;comparison page&lt;/a&gt; puts the network view simply: latency is the delay in network communication, throughput is the average volume of data that actually passes over a period, and bandwidth is the theoretical maximum (checked on 16 October 2026). Much of what you find online uses that network sense. Load testing uses the same words for requests rather than packets, which is why it pays to say which one a report means.&lt;/p&gt;
&lt;h2&gt;What does &amp;quot;latency&amp;quot; mean in a load testing tool?&lt;/h2&gt;
&lt;p&gt;It depends on the tool. In JMeter, Latency is the time to the first part of the response, while the full response time is called Elapsed time. In k6, &lt;code&gt;http_req_duration&lt;/code&gt; covers sending, waiting and receiving, and the time to first byte is a separate metric.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://jmeter.apache.org/usermanual/glossary.html&quot;&gt;JMeter glossary&lt;/a&gt; measures elapsed time &amp;quot;from just before sending the request to just after the last response has been received&amp;quot; and latency to &amp;quot;just after the first response has been received&amp;quot;. The &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/metrics/reference/&quot;&gt;k6 metrics reference&lt;/a&gt; defines &lt;code&gt;http_req_duration&lt;/code&gt; as &lt;code&gt;http_req_sending + http_req_waiting + http_req_receiving&lt;/code&gt;, without DNS lookup or connection time, and calls &lt;code&gt;http_req_waiting&lt;/code&gt; the time to first byte (both checked on 16 October 2026). So JMeter&amp;#39;s Latency lines up with k6&amp;#39;s &lt;code&gt;http_req_waiting&lt;/code&gt;, not with &lt;code&gt;http_req_duration&lt;/code&gt;. Compare a JMeter &amp;quot;latency&amp;quot; with a k6 &amp;quot;duration&amp;quot; and the JMeter number will look better without being better.&lt;/p&gt;
&lt;h2&gt;How do throughput and latency change as load grows?&lt;/h2&gt;
&lt;p&gt;At first, more load means more throughput and latency stays roughly flat. Once the system reaches its capacity, throughput stops growing and every extra user only adds waiting, so latency rises with the load.&lt;/p&gt;
&lt;p&gt;A worked example, arithmetic only. A service handles at most 200 requests per second and answers in 0.1 seconds when it is not busy. Each virtual user sends one request, then pauses for 1 second. With a fixed number of users, the closed-model form of Little&amp;#39;s law ties them together: users = requests per second × (latency + think time), as our &lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law article&lt;/a&gt; shows step by step.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Virtual users&lt;/th&gt;
&lt;th&gt;Throughput (requests/s)&lt;/th&gt;
&lt;th&gt;Latency (s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;136&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Up to 220 users, throughput grows in step with them. After that it is pinned at 200, so latency has to absorb the rest: 300 users need 300 ÷ 200 = 1.5 seconds per loop, which leaves 0.5 seconds of latency. Real systems typically bend more gradually, because queues start to form before the exact ceiling. With an arrival-rate test the picture is harsher: if requests arrive faster than 200 per second, the backlog keeps growing for as long as the excess lasts, which is the difference the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; article explains.&lt;/p&gt;
&lt;h2&gt;How do you find the knee in your own results?&lt;/h2&gt;
&lt;p&gt;Raise the load in steps, hold each step long enough to settle, and record throughput and a latency percentile for each one. The knee is the first step where throughput stops rising with the load while latency starts to climb.&lt;/p&gt;
&lt;p&gt;A few things make that reading reliable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hold each step for a few minutes so throughput is measured on a steady stretch, not a ramp.&lt;/li&gt;
&lt;li&gt;Read a percentile, not only the average; an average can stay calm while the slowest few percent of requests get much slower.&lt;/li&gt;
&lt;li&gt;Check errors at every step. Fast failures count as completed requests with short times, so a failing system can show rising throughput and falling latency.&lt;/li&gt;
&lt;li&gt;Watch the load generator. If its CPU is saturated, the ceiling you found may be its own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LoadFocus&amp;#39;s &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/response-time-vs-virtual-users?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=throughput-vs-latency&quot;&gt;response time vs virtual users chart&lt;/a&gt; plots average and maximum response time against active virtual users, one point per user level, which is where the knee shows up in a stepped test.&lt;/p&gt;
&lt;h2&gt;Can you raise throughput without raising latency?&lt;/h2&gt;
&lt;p&gt;Sometimes. Adding capacity moves the ceiling, so latency stays flat for longer. Batching usually trades the other way: it raises throughput by making individual requests wait.&lt;/p&gt;
&lt;p&gt;TCP&amp;#39;s Nagle algorithm is the classic example of that trade. &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc896.html&quot;&gt;RFC 896&lt;/a&gt; holds back new segments while earlier data on the connection is still unacknowledged, so the network carries fewer, fuller packets and each small write may wait (checked on 16 October 2026). Request batching and write buffering work the same way: more done per second, more time per item. When you change one of them, rerun the stepped test and compare both curves, not just one number.&lt;/p&gt;
&lt;h2&gt;Which one belongs in your pass/fail criteria?&lt;/h2&gt;
&lt;p&gt;Both, tied together. A latency target means little without the throughput it was measured at, and a throughput target means little if latency doubled to reach it. State them as a pair, such as p95 under 300 milliseconds at 200 requests per second with under 1% errors.&lt;/p&gt;
&lt;p&gt;Our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;load test pass/fail criteria&lt;/a&gt; shows how to write and automate that pair. If your tool waits for slow responses before sending the next request, its latency figures can understate the problem; &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt; explains how that happens and how to avoid it. LoadFocus&amp;#39;s &lt;a href=&quot;https://loadfocus.com/glossary/what-is-latency?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=throughput-vs-latency&quot;&gt;glossary entry on latency&lt;/a&gt; breaks down where latency comes from and why percentiles matter.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Does low latency mean high throughput?&lt;/strong&gt;
Not necessarily. A system can answer each request quickly but handle few at once, or handle many at once while each one waits. You only know both after measuring both under the same load, ideally across several load levels.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between latency and response time?&lt;/strong&gt;
In everyday use they often mean the same thing. In JMeter, Latency is the time to the first part of the response and Elapsed time is the full response time, so check which one a report shows before comparing tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does throughput stop increasing in a load test?&lt;/strong&gt;
Something has reached its limit: CPU, a database, a connection pool, a rate limit, or the load generator itself. From that point extra users only queue, which shows up as rising latency at flat throughput.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score: what it is and how to calculate it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/amdahls-law/&quot;&gt;Amdahl&amp;#39;s law: formula, examples and scaling limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Gatling vs Locust: JVM simulations or Python locustfiles?</title><link>https://loadtest.app/gatling-vs-locust/</link><guid isPermaLink="true">https://loadtest.app/gatling-vs-locust/</guid><description>Gatling vs Locust compared on licence, runtime, test code, load model, scaling out, pass/fail checks and reports, with advice on when each one fits.</description><pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Gatling and Locust are both free, open source tools that keep load tests as code in your repository. Pick Gatling if your team works on the JVM or you want open and closed injection profiles, assertions and an HTML report in one package. Pick Locust if your team writes Python, wants a web UI to start and watch a run, and needs distributed mode without a commercial edition.&lt;/p&gt;
&lt;p&gt;The facts in this Gatling vs Locust comparison come from each project&amp;#39;s own documentation and repository, checked on 13 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gatling&lt;/th&gt;
&lt;th&gt;Locust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Apache 2.0; gRPC, MQTT and GraphQL under a separate component licence (&lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;MIT (&lt;a href=&quot;https://github.com/locustio/locust&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;JVM, OpenJDK LTS 11 to 25 (release 3.16.0)&lt;/td&gt;
&lt;td&gt;Python 3.11 or later (release 2.46.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test code&lt;/td&gt;
&lt;td&gt;Java, Kotlin, Scala, JavaScript or TypeScript&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in protocols&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, WebSocket, Server-Sent Events, JMS&lt;/td&gt;
&lt;td&gt;HTTP and HTTPS; others through a Python client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;Open or closed injection profiles&lt;/td&gt;
&lt;td&gt;A pool of users, so a closed model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass/fail&lt;/td&gt;
&lt;td&gt;Assertions, evaluated after the run&lt;/td&gt;
&lt;td&gt;Exit code 1 on failed requests, or your own listener&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reports&lt;/td&gt;
&lt;td&gt;HTML report after each run&lt;/td&gt;
&lt;td&gt;Live web UI charts, CSV files, HTML report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;Gatling Enterprise&lt;/td&gt;
&lt;td&gt;Built-in master and workers, Kubernetes operator&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between Gatling and Locust?&lt;/h2&gt;
&lt;p&gt;Gatling runs simulations on the JVM, and Locust runs plain Python programs. Gatling gives you a load model and a report out of the box, and Locust gives you test logic you write yourself.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;README&lt;/a&gt; describes a non-blocking engine built on Netty and an SDK in five languages: Java, Kotlin and Scala run through Maven, Gradle or sbt, and JavaScript and TypeScript through a separate SDK. Its &lt;a href=&quot;https://docs.gatling.io/reference/deploy/install-local/&quot;&gt;installation page&lt;/a&gt; asks for a 64-bit OpenJDK LTS from 11 to 25, and for Node.js 24 or later with the JavaScript SDK.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s README calls it a way to write scalable load tests in plain Python, and its &lt;a href=&quot;https://raw.githubusercontent.com/locustio/locust/master/pyproject.toml&quot;&gt;package metadata&lt;/a&gt; requires Python 3.11 or later. The &lt;a href=&quot;https://docs.locust.io/en/stable/what-is-locust.html&quot;&gt;What is Locust&lt;/a&gt; page says it is event-based on gevent, so one process can handle many thousands of concurrent users (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you write the same test in each tool?&lt;/h2&gt;
&lt;p&gt;In Gatling you chain requests into a scenario and give it an injection profile. In Locust you write a user class with tasks, and a wait time between them.&lt;/p&gt;
&lt;p&gt;Fifty concurrent users who list users, then pause one to three seconds, for five minutes. In Gatling&amp;#39;s Java SDK (imports left out):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ApiSimulation extends Simulation {
  HttpProtocolBuilder api =
      http.baseUrl(&amp;quot;https://staging.example.com&amp;quot;);

  ScenarioBuilder users = scenario(&amp;quot;API users&amp;quot;)
      .exec(http(&amp;quot;list users&amp;quot;).get(&amp;quot;/api/users&amp;quot;))
      .pause(1, 3);

  {
    setUp(users.injectClosed(
        constantConcurrentUsers(50).during(300)))
      .protocols(api);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same test in Locust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from locust import HttpUser, task, between


class ApiUser(HttpUser):
    wait_time = between(1, 3)

    @task
    def list_users(self):
        self.client.get(&amp;quot;/api/users&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;injection reference&lt;/a&gt; lists &lt;code&gt;constantConcurrentUsers&lt;/code&gt; among the closed-model steps. Locust takes the user count and run time as flags (checked on 13 October 2026):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;locust -f locustfile.py --headless -u 50 -r 5 -t 5m \
  --host https://staging.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How do Gatling and Locust model load?&lt;/h2&gt;
&lt;p&gt;Gatling offers both open and closed models, chosen by the method you call. Locust runs a pool of users, which is a closed model.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/testing-concepts/workload-models/&quot;&gt;workload models page&lt;/a&gt; defines closed systems as ones that cap concurrent users and open systems as ones where users keep arriving however slowly the application responds, and says most websites are open. &lt;code&gt;injectOpen&lt;/code&gt; takes arrival rates and &lt;code&gt;injectClosed&lt;/code&gt; takes concurrency targets.&lt;/p&gt;
&lt;p&gt;In Locust, the &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;locustfile guide&lt;/a&gt; says wait time can only constrain throughput and cannot launch new users to reach a target rate. If an iteration takes longer than the wait allows, the rate drops below the target (checked on 13 October 2026). Our guide to the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains why that matters.&lt;/p&gt;
&lt;h2&gt;How do you run a bigger test than one machine can generate?&lt;/h2&gt;
&lt;p&gt;Locust has distributed mode built in. Gatling&amp;#39;s open source edition generates load from one machine, and distributed runs need Gatling Enterprise.&lt;/p&gt;
&lt;p&gt;Locust starts one process with &lt;code&gt;--master&lt;/code&gt; and any number of &lt;code&gt;--worker&lt;/code&gt; processes. Its &lt;a href=&quot;https://docs.locust.io/en/stable/running-distributed.html&quot;&gt;distributed docs&lt;/a&gt; say a Python process cannot use more than one core, so you run one worker per core, and &lt;code&gt;--processes&lt;/code&gt; starts several on one machine. A &lt;a href=&quot;https://docs.locust.io/en/stable/kubernetes-operator.html&quot;&gt;Kubernetes operator&lt;/a&gt; manages distributed runs inside a cluster.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/tutorials/faq/&quot;&gt;FAQ&lt;/a&gt; says the Community Edition does not support distributed testing, and the README describes Enterprise as running on managed load generators or private infrastructure (checked on 13 October 2026).&lt;/p&gt;
&lt;p&gt;There is a way out of both. LoadFocus, which publishes this site, does not run Gatling simulations or Locust tests. If you would rather not run either at scale, it runs &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gatling-vs-locust&quot;&gt;k6 scripts&lt;/a&gt; and &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gatling-vs-locust&quot;&gt;JMeter test plans&lt;/a&gt; from 25 cloud regions. Its k6 validator warns that arrival-rate scenarios run there with a fixed number of users, so they do not hold a request rate. Moving from either tool means rewriting the test.&lt;/p&gt;
&lt;h2&gt;How do they decide whether a test passed, and what reports do you get?&lt;/h2&gt;
&lt;p&gt;Gatling evaluates assertions after the run and writes an HTML report every time. Locust exits with code 1 if any request failed, and you add your own checks as a listener.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/concepts/assertions/&quot;&gt;assertions&lt;/a&gt; are evaluated after the simulation, and one failed assertion fails the simulation. Its &lt;a href=&quot;https://docs.gatling.io/reference/stats/reports/oss/&quot;&gt;HTML report&lt;/a&gt; charts response time percentiles, users started per second, concurrent users and requests per second.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/running-without-web-ui.html&quot;&gt;headless docs&lt;/a&gt; set exit code 1 when any sample failed, and show a listener that fails the run on failure ratio, average response time or 95th percentile. The &lt;a href=&quot;https://docs.locust.io/en/stable/configuration.html&quot;&gt;configuration reference&lt;/a&gt; lists &lt;code&gt;--csv&lt;/code&gt; for statistics files and &lt;code&gt;--html&lt;/code&gt; for a report (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;Which protocols can each tool test?&lt;/h2&gt;
&lt;p&gt;Gatling ships more protocols, with limits on the newer ones. Locust ships HTTP and HTTPS and expects a Python client for the rest.&lt;/p&gt;
&lt;p&gt;The Gatling README lists HTTP/1.1, HTTP/2, WebSocket, Server-Sent Events and JMS under Apache 2.0 with no limits. gRPC, MQTT and GraphQL sit under the Gatling Enterprise Component License, and Community Edition tests with them are capped at 5 concurrent users and 5 minutes.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/testing-other-systems.html&quot;&gt;page on other systems&lt;/a&gt; shows how to wrap a protocol library and report each call, with gRPC and MQTT examples. The library has to work with gevent, which cannot patch I/O done in compiled C code, and locust-plugins adds Kafka, Selenium and Playwright users (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Choose by language and by how you load the system. Gatling suits JVM teams and open-model tests, and Locust suits Python teams and tests with custom logic.&lt;/p&gt;
&lt;p&gt;Choose Gatling instead of Locust when your team lives on the JVM and wants Java, Kotlin or Scala tests built with Maven, Gradle or sbt, when you need an open model such as a fixed arrival rate, when you need JMS or Server-Sent Events, or when you want assertions and a report with no extra code.&lt;/p&gt;
&lt;p&gt;Choose Locust instead of Gatling when your team writes Python and wants tests that reuse an internal Python client, when you want a web UI to start a run and change the load while it runs, when you need distributed mode in the free tool, or when the system is mostly waiting users and your protocol has a gevent-friendly Python library.&lt;/p&gt;
&lt;p&gt;For a GUI test builder look at JMeter, and for a single binary with no JVM or Python look at k6.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Gatling faster than Locust?&lt;/strong&gt;
We have not measured this ourselves, so we will not name a winner. The fair test is the same scenario with both tools on your own hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Locust run Gatling simulations?&lt;/strong&gt;
Not according to Locust&amp;#39;s documentation, which describes tests only as Python code. A Gatling simulation has to be rewritten as a locustfile, and the reverse is true as well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Gatling or Locust tests?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To spread a Gatling simulation across machines use Gatling Enterprise, and for Locust use its master and worker mode or its Kubernetes operator.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling load testing: a review of the JVM tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust load testing: a review of the Python tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6: JVM simulations or a single Go binary?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter: Python scripts or JMeter test plans?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>JMeter vs Postman: which tool for load testing?</title><link>https://loadtest.app/jmeter-vs-postman/</link><guid isPermaLink="true">https://loadtest.app/jmeter-vs-postman/</guid><description>JMeter vs Postman for load testing: what Postman&apos;s virtual users, cloud runs and pricing do, where they stop, and when JMeter is the better choice.</description><pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Postman is an API client built for functional checks, collections and team collaboration, and it has added a performance test mode on top. Apache JMeter is a load generator from the start. Use Postman when you want to reuse the collection you already have for a quick load check or a CI smoke test. Use JMeter when load testing is the job: many protocols, large user counts, detailed reports and distributed generation.&lt;/p&gt;
&lt;p&gt;The facts about Postman come from its documentation and pricing page, and the facts about JMeter from its own site, all checked on 13 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Postman&lt;/th&gt;
&lt;th&gt;Apache JMeter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Main purpose&lt;/td&gt;
&lt;td&gt;API client, functional and collection testing, with a performance mode&lt;/td&gt;
&lt;td&gt;Load and performance testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test definition&lt;/td&gt;
&lt;td&gt;A collection of requests with scripts and &lt;code&gt;pm.test()&lt;/code&gt; assertions&lt;/td&gt;
&lt;td&gt;A test plan built in a GUI and saved as a &lt;code&gt;.jmx&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual users&lt;/td&gt;
&lt;td&gt;Each runs the collection in a loop; local runs are limited by your machine&lt;/td&gt;
&lt;td&gt;One Java thread per user in a thread group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load profiles&lt;/td&gt;
&lt;td&gt;Fixed, ramp up, spike and peak&lt;/td&gt;
&lt;td&gt;Thread groups with a ramp-up period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where load comes from&lt;/td&gt;
&lt;td&gt;Your machine or CI runner, or Postman&amp;#39;s cloud on paid plans&lt;/td&gt;
&lt;td&gt;Your machine, or remote JMeter servers you run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of load&lt;/td&gt;
&lt;td&gt;Local runs on all plans; cloud runs at $0.04 per virtual user hour&lt;/td&gt;
&lt;td&gt;Free, Apache 2.0 licence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass or fail in CI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postman performance run&lt;/code&gt; with &lt;code&gt;--pass-if&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CLI mode (&lt;code&gt;jmeter -n&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What does Postman&amp;#39;s performance testing do?&lt;/h2&gt;
&lt;p&gt;It runs the requests of a collection with several virtual users in parallel, for a set number of minutes. Each user repeats the requests in order, and your existing scripts and assertions run under load.&lt;/p&gt;
&lt;p&gt;Postman&amp;#39;s &lt;a href=&quot;https://learning.postman.com/docs/tests-and-scripts/performance-testing/testing-api-performance&quot;&gt;performance testing docs&lt;/a&gt; say the collection you use for functional tests is also your load test, with no separate script or language to maintain (checked on 13 October 2026). You pick a duration and a number of virtual users, then a fixed, ramp up, spike or peak profile, as the &lt;a href=&quot;https://learning.postman.com/docs/tests-and-scripts/performance-testing/performance-test-configuration&quot;&gt;configuration page&lt;/a&gt; describes.&lt;/p&gt;
&lt;h2&gt;What are the limits of Postman for load testing?&lt;/h2&gt;
&lt;p&gt;The main limit is where the load comes from. A local run is capped by the machine that runs it, and anything beyond that needs a paid plan.&lt;/p&gt;
&lt;p&gt;For local runs, Postman&amp;#39;s docs say the number of virtual users depends on available system resources and the collection used, that pre-request and post-response scripts reduce how many users you can simulate, and that the run stops if the machine passes its resource threshold. They suggest a first test of 10 to 20 virtual users (checked on 13 October 2026). Past that, the load generator itself can distort your numbers, which is why Postman also reports the peak CPU and memory of the test machine.&lt;/p&gt;
&lt;p&gt;Data is the next limit. CSV and JSON data files work for local runs only, while datasets work for local and cloud runs, according to the same configuration page.&lt;/p&gt;
&lt;h2&gt;How much does Postman load testing cost?&lt;/h2&gt;
&lt;p&gt;Local performance runs are included in every plan. Cloud runs are for paid plans and are billed by usage in virtual user hours.&lt;/p&gt;
&lt;p&gt;Postman&amp;#39;s &lt;a href=&quot;https://www.postman.com/pricing/&quot;&gt;pricing page&lt;/a&gt; lists unlimited local performance testing and cloud performance testing at $0.04 per virtual user hour (checked on 13 October 2026). At that listed rate, 500 virtual users for one hour is 500 virtual user hours, or $20, on top of the plan fee. Static IP addresses for reaching targets behind a firewall are an Enterprise feature. We did not price the plans themselves, so check the page for the current per-user fees.&lt;/p&gt;
&lt;h2&gt;Which protocols can you load test?&lt;/h2&gt;
&lt;p&gt;JMeter has a long, documented list. For Postman, we found no list of protocols supported in performance runs, so we make no claim about them.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/&quot;&gt;home page&lt;/a&gt; lists HTTP and HTTPS, SOAP and REST, FTP, databases through JDBC, LDAP, JMS messaging, SMTP, POP3 and IMAP mail, native commands and shell scripts, TCP and Java objects (checked on 13 October 2026). Postman handles other request types in the app, such as GraphQL, gRPC and WebSocket, but that does not tell you which of them run under load. Test one before you rely on it.&lt;/p&gt;
&lt;h2&gt;How do they scale beyond one machine?&lt;/h2&gt;
&lt;p&gt;Postman scales through its own cloud, JMeter through servers you run yourself. The first is easier to start, the second gives you control.&lt;/p&gt;
&lt;p&gt;On a paid plan, Postman&amp;#39;s cloud runs split the load across managed workers, so one machine no longer limits the test. JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/remote-test.html&quot;&gt;remote testing mode&lt;/a&gt; lets one client drive many JMeter servers. Every node needs the same JMeter version, a keystore for RMI over SSL unless you disable it, and its own copy of any data files (checked on 13 October 2026).&lt;/p&gt;
&lt;p&gt;There is a third route for JMeter test plans. LoadFocus, which publishes this site, &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=jmeter-vs-postman&quot;&gt;runs existing JMeter test plans&lt;/a&gt; from 25 cloud regions. You upload the &lt;code&gt;.jmx&lt;/code&gt; file and set virtual users, duration and ramp-up, with no remote servers to configure. LoadFocus runs JMeter and k6 only, so it does not run a Postman collection.&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Choose Postman instead of JMeter for functional and contract checks, for a team that shares collections in a workspace, and for a quick smoke run of an API you already test. Choose JMeter when load testing is the main goal, or when you need several protocols, large user counts, or tests that stay in your own repository.&lt;/p&gt;
&lt;p&gt;Postman fits a short check in CI: &lt;code&gt;postman performance run&lt;/code&gt; exits with a non-zero code when a threshold such as the 95th percentile is missed, which fails the build. JMeter is the better choice for database or messaging load, for long soak tests, and for the HTML dashboard with APDEX and percentile tables that &lt;a href=&quot;https://jmeter.apache.org/usermanual/generating-dashboard.html&quot;&gt;its docs&lt;/a&gt; describe.&lt;/p&gt;
&lt;p&gt;Many teams use both: Postman to build and verify the API checks, JMeter to put real volume behind the critical flows.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Can Postman replace JMeter for load testing?&lt;/strong&gt;
For small checks on HTTP APIs it can, because the collection you already have is the test. For high concurrency, other protocols or long runs, the documented limits on local runs and the usage pricing of cloud runs make JMeter the safer base.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Postman run a JMeter .jmx file?&lt;/strong&gt;
We found nothing in its documentation that imports a JMeter test plan. Postman performance tests are built from collections, so a &lt;code&gt;.jmx&lt;/code&gt; file would have to be rebuilt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is JMeter free?&lt;/strong&gt;
Yes. The current release on the &lt;a href=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot;&gt;download page&lt;/a&gt; is 5.6.3, which needs Java 8 or later (checked on 13 October 2026). You pay for the machines that generate load, or for a cloud service that runs your plans.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-jmeter/&quot;&gt;Gatling vs JMeter: tests as code or a GUI test plan?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter: Python scripts or JMeter test plans?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score: what it is and how to calculate it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>k6 vs Locust: JavaScript scripts or Python users?</title><link>https://loadtest.app/k6-vs-locust/</link><guid isPermaLink="true">https://loadtest.app/k6-vs-locust/</guid><description>k6 vs Locust compared on licence, scripting language, load models, pass/fail checks, protocols and scaling out, with advice on when each tool fits.</description><pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;k6 and Locust are both open source tools that keep a load test as code in your repository, and either one will load test an HTTP API well. Pick Locust if your team writes Python and wants plain Python for test logic. Pick k6 if you write JavaScript, want pass/fail thresholds built in, or need a test that holds an arrival rate.&lt;/p&gt;
&lt;p&gt;The facts in this k6 vs Locust comparison come from each project&amp;#39;s own documentation and repository, checked on 13 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;k6&lt;/th&gt;
&lt;th&gt;Locust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;AGPL-3.0 (&lt;a href=&quot;https://github.com/grafana/k6&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;MIT (&lt;a href=&quot;https://github.com/locustio/locust&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;One Go binary with an embedded JavaScript engine (release v2.3.0)&lt;/td&gt;
&lt;td&gt;Python 3.11 or later (release 2.46.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test scripts&lt;/td&gt;
&lt;td&gt;JavaScript, or TypeScript with the types stripped&lt;/td&gt;
&lt;td&gt;Python classes in a locustfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in protocols&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, WebSockets, gRPC&lt;/td&gt;
&lt;td&gt;HTTP and HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;Six executors, two of them arrival-rate (open model)&lt;/td&gt;
&lt;td&gt;A pool of users, each pausing for a wait time (closed model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass/fail&lt;/td&gt;
&lt;td&gt;Thresholds, which can abort a run early&lt;/td&gt;
&lt;td&gt;Exit code 1 on failed requests; other limits via a listener&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser testing&lt;/td&gt;
&lt;td&gt;Built-in browser module&lt;/td&gt;
&lt;td&gt;Browser users via the separate &lt;a href=&quot;https://github.com/SvenskaSpel/locust-plugins&quot;&gt;locust-plugins&lt;/a&gt; project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;k6-operator on Kubernetes, or Grafana Cloud k6&lt;/td&gt;
&lt;td&gt;Master and workers, a Kubernetes operator, or Azure Load Testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between k6 and Locust?&lt;/h2&gt;
&lt;p&gt;The main difference is the language you write tests in. k6 runs JavaScript in a compiled Go program, while Locust runs your Python code and builds on gevent for many concurrent users.&lt;/p&gt;
&lt;p&gt;k6 installs as &lt;a href=&quot;https://grafana.com/docs/k6/latest/set-up/install-k6/&quot;&gt;a package or a standalone binary&lt;/a&gt; with no other runtime. It also runs &lt;code&gt;.ts&lt;/code&gt; files, though its &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/javascript-typescript-compatibility-mode/&quot;&gt;TypeScript support&lt;/a&gt; strips the types without checking them. Locust installs with &lt;code&gt;pip install locust&lt;/code&gt;, and its &lt;a href=&quot;https://docs.locust.io/en/stable/what-is-locust.html&quot;&gt;What is Locust&lt;/a&gt; page says tests are plain Python that you keep in version control like any other code (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you write the same test in each tool?&lt;/h2&gt;
&lt;p&gt;Both express the test as a short program. Here is a test with 50 users for five minutes that fails if the 95th percentile tops 500 ms or more than 1% of requests fail.&lt;/p&gt;
&lt;p&gt;In k6 the goals are part of the script, as &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;
import { sleep } from &amp;quot;k6&amp;quot;;

export const options = {
  vus: 50,
  duration: &amp;quot;5m&amp;quot;,
  thresholds: {
    http_req_duration: [&amp;quot;p(95)&amp;lt;500&amp;quot;],
    http_req_failed: [&amp;quot;rate&amp;lt;0.01&amp;quot;],
  },
};

export default function () {
  http.get(&amp;quot;https://staging.example.com/api/users&amp;quot;);
  sleep(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Locust the user is a class, and the same goals go in a listener, following the pattern in the &lt;a href=&quot;https://docs.locust.io/en/stable/running-without-web-ui.html&quot;&gt;headless mode docs&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import logging
from locust import HttpUser, between, events, task


class ApiUser(HttpUser):
    wait_time = between(1, 2)

    @task
    def list_users(self):
        self.client.get(&amp;quot;/api/users&amp;quot;)


@events.quitting.add_listener
def _(environment, **kw):
    stats = environment.stats.total
    if stats.fail_ratio &amp;gt; 0.01:
        environment.process_exit_code = 1
    elif stats.get_response_time_percentile(0.95) &amp;gt; 500:
        environment.process_exit_code = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run them with &lt;code&gt;k6 run script.js&lt;/code&gt; and &lt;code&gt;locust -f locustfile.py --headless -u 50 -r 10 -t 5m --host https://staging.example.com&lt;/code&gt; (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;How do k6 and Locust model load?&lt;/h2&gt;
&lt;p&gt;k6 supports both workload models, while Locust describes load as a fixed number of users. In k6 you choose the model through the executor, and in Locust through the user count and wait time.&lt;/p&gt;
&lt;p&gt;k6 has &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/&quot;&gt;six executors&lt;/a&gt;. Four are closed-model, where a virtual user starts its next iteration only when the last one finishes, and the two arrival-rate executors are &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot;&gt;open-model&lt;/a&gt;, where iterations start independently of how long earlier ones take. Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;locustfile guide&lt;/a&gt; says wait time can only constrain throughput and cannot launch new users to reach a target rate (checked on 13 October 2026).&lt;/p&gt;
&lt;p&gt;If your goal is &amp;quot;300 requests arrive every second whatever the response time&amp;quot;, k6 can express that directly. Our guide to the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains when each model fits.&lt;/p&gt;
&lt;h2&gt;How do k6 and Locust decide whether a test passed?&lt;/h2&gt;
&lt;p&gt;k6 has pass/fail criteria as a core feature, and Locust leaves most of it to your code. A failed k6 threshold ends the run with a non-zero exit code, and &lt;code&gt;abortOnFail&lt;/code&gt; stops the test the moment it fails.&lt;/p&gt;
&lt;p&gt;Locust exits with code 1 if any request failed. For limits on the failure ratio, the average or the 95th percentile, its docs show a listener like the one above that sets the exit code yourself (checked on 13 October 2026). Our guide to &lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;pass/fail criteria for load tests&lt;/a&gt; covers how to pick the limits.&lt;/p&gt;
&lt;h2&gt;Which protocols does each tool cover?&lt;/h2&gt;
&lt;p&gt;k6 ships four protocols and a browser module, while Locust ships HTTP and HTTPS and lets you wrap any Python client for the rest.&lt;/p&gt;
&lt;p&gt;k6 lists HTTP/1.1, HTTP/2, WebSockets and gRPC as &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/protocols/&quot;&gt;built-in protocols&lt;/a&gt;, with more through xk6 extensions, and a &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6-browser/&quot;&gt;browser module&lt;/a&gt; that drives a Chromium-based browser. Locust&amp;#39;s page on &lt;a href=&quot;https://docs.locust.io/en/stable/testing-other-systems.html&quot;&gt;other systems&lt;/a&gt; says it comes with built-in support only for HTTP/HTTPS but can be extended to test almost any system, by wrapping a protocol library and reporting each call as a request. The library has to cooperate with gevent (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you run a bigger test than one machine can generate?&lt;/h2&gt;
&lt;p&gt;Locust scales with a master and workers, and k6 with a Kubernetes operator or a hosted service. Both can split one test across many machines.&lt;/p&gt;
&lt;p&gt;The Locust &lt;a href=&quot;https://docs.locust.io/en/stable/running-distributed.html&quot;&gt;distributed docs&lt;/a&gt; say one process can make more than a thousand requests per second for a simple test plan, so larger tests run one worker per processor core across as many machines as needed. The &lt;a href=&quot;https://grafana.com/docs/k6/latest/testing-guides/running-distributed-tests/&quot;&gt;k6-operator&lt;/a&gt; splits one test across several k6 instances in a Kubernetes cluster, and Grafana Cloud k6 is the hosted route (checked on 13 October 2026).&lt;/p&gt;
&lt;p&gt;There is a third route for k6 scripts. LoadFocus, which publishes this site, &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=k6-vs-locust&quot;&gt;runs k6 scripts&lt;/a&gt; from 25 cloud regions, with the users and duration taken from the test&amp;#39;s settings. Its script validator warns that arrival-rate scenarios run there with a fixed number of users, so an open-model script does not hold its arrival rate on LoadFocus. LoadFocus does not run Locust: it runs k6 scripts and JMeter test plans only.&lt;/p&gt;
&lt;h2&gt;What reports do you get?&lt;/h2&gt;
&lt;p&gt;k6 prints an end-of-test summary and can export an HTML report from its web dashboard. Locust shows live charts in its web UI and writes files on request.&lt;/p&gt;
&lt;p&gt;k6&amp;#39;s &lt;a href=&quot;https://grafana.com/docs/k6/latest/results-output/end-of-test/&quot;&gt;end-of-test summary&lt;/a&gt; goes to the terminal, and its &lt;a href=&quot;https://grafana.com/docs/k6/latest/results-output/web-dashboard/&quot;&gt;web dashboard&lt;/a&gt; exports a self-contained HTML report when you set &lt;code&gt;K6_WEB_DASHBOARD_EXPORT&lt;/code&gt;. Locust serves a web UI on port 8089 with live charts, and its &lt;a href=&quot;https://docs.locust.io/en/stable/configuration.html&quot;&gt;configuration reference&lt;/a&gt; lists &lt;code&gt;--csv&lt;/code&gt;, &lt;code&gt;--html&lt;/code&gt; and &lt;code&gt;--json&lt;/code&gt; output (checked on 13 October 2026).&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Locust fits Python teams, and k6 fits teams that want built-in thresholds and an open workload model. Protocols and scaling settle most other cases.&lt;/p&gt;
&lt;p&gt;Choose Locust instead of k6 when your team writes Python and wants loops, branches, generated data and its own API client in plain Python, when you want to reuse Python libraries for a protocol, or when you want a web UI with live charts to explore a system. Choose k6 instead of Locust when your team writes JavaScript or TypeScript, when thresholds should fail a CI build and stop a bad run early, when you need an arrival rate held constant, or when you want gRPC and browser tests in the same tool.&lt;/p&gt;
&lt;p&gt;For a wider view, see our review of &lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust load testing&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Locust faster than k6?&lt;/strong&gt;
We have not measured this ourselves, so we will not name a winner. The fair test is the same scenario with both tools on your own hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Locust run k6 scripts?&lt;/strong&gt;
Not according to the Locust docs, which describe tests as Python code in a locustfile. A k6 script is JavaScript, so it has to be rewritten as a Python user class, and the thresholds become a listener.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Locust tests?&lt;/strong&gt;
No. LoadFocus runs k6 scripts and JMeter test plans. To scale a Locust test, use its master and worker mode, the Kubernetes operator or Azure Load Testing.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust load testing: a review of the Python tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter: Python scripts or JMeter test plans?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6: JVM simulations or a single Go binary?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/azure-load-testing/&quot;&gt;Azure Load Testing: a review of Microsoft&amp;#39;s service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Gatling load testing: a review of the JVM tool</title><link>https://loadtest.app/gatling-load-testing/</link><guid isPermaLink="true">https://loadtest.app/gatling-load-testing/</guid><description>Gatling load testing reviewed: simulations, open and closed injection, checks, assertions, HTML reports, build plugins, CI, limits and when k6 fits better.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Gatling is a free, Apache 2.0 load testing tool in which every test is a simulation written as code and run on the JVM, with an HTML report after each run. Gatling load testing suits developer teams who want tests in the repository, open or closed workload models and assertions that fail the build. Teams that want a GUI test builder or many protocols are better served by JMeter, and teams that want one binary with no JVM by k6.&lt;/p&gt;
&lt;p&gt;We have not run Gatling for this review. The facts come from Gatling&amp;#39;s own documentation, repository and release pages, checked on 9 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Gatling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Apache 2.0, except the gRPC, MQTT and GraphQL plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;JVM, 64-bit OpenJDK LTS 11 to 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current release&lt;/td&gt;
&lt;td&gt;3.16.0 (29 September 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written in&lt;/td&gt;
&lt;td&gt;Java, Kotlin, Scala, JavaScript, TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in protocols&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, WebSocket, SSE, JMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results&lt;/td&gt;
&lt;td&gt;HTML report per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build and CI&lt;/td&gt;
&lt;td&gt;Maven, Gradle, sbt, npm plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;Gatling Enterprise (commercial)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Who makes Gatling, and is it still maintained?&lt;/h2&gt;
&lt;p&gt;Gatling Corp, a French company, develops Gatling and owns its copyright. It is actively maintained, with three JVM releases so far in 2026.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.gatling.io/project/licenses/project-licenses/&quot;&gt;licences page&lt;/a&gt; gives Gatling Corp the copyright. &lt;a href=&quot;https://repo1.maven.org/maven2/io/gatling/gatling-core/&quot;&gt;Maven Central&lt;/a&gt; lists 3.15.0 in February, 3.15.1 in May and 3.16.0 on 29 September 2026, and the JavaScript SDK reached &lt;a href=&quot;https://github.com/gatling/gatling-js/releases/tag/v3.16.1&quot;&gt;3.16.1&lt;/a&gt; on 6 October. The &lt;a href=&quot;https://docs.gatling.io/release-notes/gatling/whats-new/3.16/&quot;&gt;3.16 release notes&lt;/a&gt; add GraphQL and shared queues between virtual users. The &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;README&lt;/a&gt; calls the repository the Community Edition, free to use including commercially (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;What do you need to run Gatling?&lt;/h2&gt;
&lt;p&gt;A 64-bit JVM for Java, Kotlin or Scala, or Node.js for JavaScript and TypeScript. Either way, the simulation itself runs on a JVM.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.gatling.io/reference/deploy/install-local/&quot;&gt;installation page&lt;/a&gt; supports OpenJDK LTS versions 11 to 25 and excludes 32-bit JVMs and OpenJ9. The &lt;a href=&quot;https://docs.gatling.io/integrations/build-tools/js-cli/&quot;&gt;JavaScript CLI&lt;/a&gt; needs Node.js 24 or later and downloads a Gatling runtime bundle and, per the &lt;a href=&quot;https://github.com/gatling/gatling-js&quot;&gt;gatling-js README&lt;/a&gt;, GraalVM (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How is a Gatling simulation structured?&lt;/h2&gt;
&lt;p&gt;A simulation defines scenarios, gives each one an injection profile and attaches a protocol configuration, all registered in one &lt;code&gt;setUp&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;https://docs.gatling.io/concepts/simulation/&quot;&gt;simulation reference&lt;/a&gt;, a scenario chains requests with pauses, loops and conditions. This Java simulation saves a product id from a search, pauses one to three seconds and opens the product, while arrivals ramp to 20 users per second and hold for ten minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ShopSimulation extends Simulation {

  HttpProtocolBuilder api =
      http.baseUrl(&amp;quot;https://staging.example.com&amp;quot;);

  ScenarioBuilder shoppers = scenario(&amp;quot;Shoppers&amp;quot;)
      .exec(http(&amp;quot;search&amp;quot;).get(&amp;quot;/api/products?q=lamp&amp;quot;)
          .check(jsonPath(&amp;quot;$[0].id&amp;quot;).saveAs(&amp;quot;id&amp;quot;)))
      .pause(1, 3)
      .exec(http(&amp;quot;product&amp;quot;).get(&amp;quot;/api/products/#{id}&amp;quot;));

  {
    setUp(shoppers.injectOpen(
        rampUsersPerSec(1).to(20).during(120),
        constantUsersPerSec(20).during(600)))
      .protocols(api)
      .assertions(
          global().responseTime().percentile(95.0).lt(800));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Imports are left out, and &lt;code&gt;#{id}&lt;/code&gt; is Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/concepts/session/el/&quot;&gt;Expression Language&lt;/a&gt; reading the saved value. &lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6&lt;/a&gt; shows a JavaScript simulation.&lt;/p&gt;
&lt;h2&gt;Should you inject users open or closed?&lt;/h2&gt;
&lt;p&gt;Open, for most websites and APIs, where new users keep arriving however slow the system gets. Closed suits systems that cap concurrency, such as a ticket queue.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;injection reference&lt;/a&gt; splits the API: &lt;code&gt;injectOpen&lt;/code&gt; takes arrival rates such as &lt;code&gt;rampUsersPerSec&lt;/code&gt;, and &lt;code&gt;injectClosed&lt;/code&gt; takes concurrency targets such as &lt;code&gt;constantConcurrentUsers&lt;/code&gt;. The &lt;a href=&quot;https://docs.gatling.io/testing-concepts/workload-models/&quot;&gt;workload models page&lt;/a&gt; says most websites are open systems and one profile cannot mix the two (checked on 9 October 2026). Our &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; guide explains the choice.&lt;/p&gt;
&lt;h2&gt;How do checks, assertions and the report work?&lt;/h2&gt;
&lt;p&gt;Checks verify each response and can save values from it, while assertions judge the whole run once it ends. Every run then writes an HTML report.&lt;/p&gt;
&lt;p&gt;Without an explicit status check, Gatling &lt;a href=&quot;https://docs.gatling.io/reference/script/http/checks/&quot;&gt;checks&lt;/a&gt; that each HTTP response is 2XX or 304. &lt;a href=&quot;https://docs.gatling.io/concepts/assertions/&quot;&gt;Assertions&lt;/a&gt; test response times, request counts or requests per second, globally or per request, and one failure fails the simulation. The &lt;a href=&quot;https://docs.gatling.io/reference/stats/reports/oss/&quot;&gt;HTML report&lt;/a&gt; charts response time ranges and percentiles, users started per second, concurrent users and requests per second; its percentiles over time count successful requests only (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How does Gatling fit into a build and CI?&lt;/h2&gt;
&lt;p&gt;Through its Maven, Gradle, sbt or npm plugin, which runs in any CI system. A failed assertion fails the build.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.gatling.io/integrations/build-tools/maven-plugin/&quot;&gt;Maven plugin&lt;/a&gt; binds &lt;code&gt;gatling:test&lt;/code&gt; to the &lt;code&gt;integration-test&lt;/code&gt; phase, so &lt;code&gt;mvn verify&lt;/code&gt; runs it, and with &lt;code&gt;CI=true&lt;/code&gt; it skips the simulation prompt (the run then fails if the project has more than one simulation and none is named). The &lt;a href=&quot;https://docs.gatling.io/integrations/build-tools/gradle-plugin/&quot;&gt;Gradle plugin&lt;/a&gt; uses &lt;code&gt;gatlingRun&lt;/code&gt;, and the &lt;a href=&quot;https://docs.gatling.io/integrations/build-tools/sbt-plugin/&quot;&gt;sbt plugin&lt;/a&gt; takes Scala simulations only. Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/integrations/ci-cd/&quot;&gt;CI integrations&lt;/a&gt;, such as its GitHub Action, start Gatling Enterprise runs (checked on 9 October 2026). Our &lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;GitHub Actions guide&lt;/a&gt; covers the workflow.&lt;/p&gt;
&lt;h2&gt;Which protocols does Gatling support?&lt;/h2&gt;
&lt;p&gt;HTTP/1.1, HTTP/2, WebSocket, Server-Sent Events and JMS ship without limits. gRPC, MQTT and GraphQL are separate plugins capped at 5 users and 5 minutes outside Gatling Enterprise.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;README&lt;/a&gt; puts the first five under Apache 2.0. The &lt;a href=&quot;https://docs.gatling.io/reference/script/grpc/setup/&quot;&gt;gRPC&lt;/a&gt;, &lt;a href=&quot;https://docs.gatling.io/reference/script/mqtt/setup/&quot;&gt;MQTT&lt;/a&gt; and &lt;a href=&quot;https://docs.gatling.io/reference/script/graphql/setup/&quot;&gt;GraphQL&lt;/a&gt; setup pages put each plugin under the Gatling Enterprise Component License: unlimited on Gatling Enterprise, while a Community Edition test stops at 5 users or 5 minutes. That &lt;a href=&quot;https://docs.gatling.io/project/licenses/enterprise-component/&quot;&gt;licence&lt;/a&gt; grants free use for up to one year, only for internal testing of your own web applications; it ends as soon as a limit is reached, and Gatling may change the limits. GraphQL needs 3.16.0 or later. Its release notes and the README say the JavaScript SDK does not support it yet, although the setup page already lists an npm package, so check before planning a JavaScript GraphQL test (checked on 9 October 2026). Our guide on &lt;a href=&quot;https://loadtest.app/how-to-load-test-a-graphql-api/&quot;&gt;how to load test a GraphQL API&lt;/a&gt; works around the cap with plain HTTP requests.&lt;/p&gt;
&lt;h2&gt;What does Gatling Enterprise add?&lt;/h2&gt;
&lt;p&gt;Gatling Enterprise is the commercial edition on the same engine. It generates load from managed or private infrastructure, which the free edition does not.&lt;/p&gt;
&lt;p&gt;Per the README, Enterprise runs tests on managed load generators or in your own AWS, Azure, GCP or Kubernetes accounts, and adds live dashboards, trends, teams with SSO and CI/CD integrations. The &lt;a href=&quot;https://docs.gatling.io/tutorials/faq/&quot;&gt;FAQ&lt;/a&gt; says the Community Edition does not support distributed testing and that simulations move to Enterprise unchanged (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run Gatling simulations. It &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gatling-load-testing&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions, so for Gatling the route to more machines is Gatling Enterprise.&lt;/p&gt;
&lt;h2&gt;What does Gatling do well, and where are its limits?&lt;/h2&gt;
&lt;p&gt;It is strong on tests as reviewable code, on load models and on reports with no setup. It is weaker on protocols beyond the web and messaging, and on free scaling past one machine.&lt;/p&gt;
&lt;p&gt;The README credits its engine: non-blocking, built on Netty, with each virtual user a lightweight message. Limits the docs state themselves (checked on 9 October 2026):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gatling is not a browser: per the &lt;a href=&quot;https://docs.gatling.io/reference/script/http/protocol/&quot;&gt;HTTP protocol page&lt;/a&gt;, it runs no JavaScript.&lt;/li&gt;
&lt;li&gt;The JavaScript SDK has no &lt;a href=&quot;https://docs.gatling.io/reference/script/jms/&quot;&gt;JMS&lt;/a&gt; and cannot use npm libraries that need native binaries or Node.js APIs.&lt;/li&gt;
&lt;li&gt;HTTP caching is on by default, which can lower request counts.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;simulation.log&lt;/code&gt; format is undocumented and can change at any time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When should you choose k6, JMeter or Locust instead?&lt;/h2&gt;
&lt;p&gt;Choose another tool when its language, protocols or free scaling route fit your team better.&lt;/p&gt;
&lt;p&gt;Our &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; overview covers five others.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;k6&lt;/strong&gt; instead when you want &lt;a href=&quot;https://grafana.com/docs/k6/latest/set-up/install-k6/&quot;&gt;one binary&lt;/a&gt; in CI with no JVM, &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt; that can stop a failing run early, or gRPC among its &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/protocols/&quot;&gt;built-in protocols&lt;/a&gt; (checked on 9 October 2026). &lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6&lt;/a&gt; compares the two.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;JMeter&lt;/strong&gt; instead when testers build tests in a GUI, or you need JDBC, LDAP or FTP from its &lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;core&lt;/a&gt; (checked on 9 October 2026). LoadFocus &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gatling-load-testing&quot;&gt;runs existing JMeter test plans&lt;/a&gt; from 25 cloud regions, and &lt;a href=&quot;https://loadtest.app/gatling-vs-jmeter/&quot;&gt;Gatling vs JMeter&lt;/a&gt; goes deeper.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;Locust&lt;/strong&gt; instead when your team writes Python and wants a &lt;a href=&quot;https://docs.locust.io/en/stable/running-distributed.html&quot;&gt;distributed mode&lt;/a&gt; in the free tool (checked on 9 October 2026). Our &lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust review&lt;/a&gt; covers it.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Gatling free for commercial use?&lt;/strong&gt;
Yes. The Community Edition is Apache 2.0. The gRPC, MQTT and GraphQL plugins have their own licence and stop at 5 users and 5 minutes outside Gatling Enterprise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do you need Java to use Gatling?&lt;/strong&gt;
For Java, Kotlin or Scala, yes: a 64-bit OpenJDK LTS from 11 to 25. The JavaScript SDK needs Node.js 24 or later, and its CLI downloads the JVM runtime itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Gatling tests?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To spread a Gatling simulation across several machines, use Gatling Enterprise.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-jmeter/&quot;&gt;Gatling vs JMeter: tests as code or a GUI test plan?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6: JVM simulations or a single Go binary?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-locust/&quot;&gt;Gatling vs Locust: JVM simulations or Python locustfiles?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/nbomber-load-testing/&quot;&gt;NBomber load testing: a review of the .NET framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Gatling vs JMeter: tests as code or a GUI test plan?</title><link>https://loadtest.app/gatling-vs-jmeter/</link><guid isPermaLink="true">https://loadtest.app/gatling-vs-jmeter/</guid><description>Gatling vs JMeter compared on licence, scripting, protocols, recorders, load models, scaling out, CI and reports, with plain advice on when each one fits.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Gatling and Apache JMeter are both free, open source load testing tools that run on the JVM, and the choice comes down mostly to how your team wants to build tests. Pick Gatling if you want tests as code in Java, Kotlin, Scala, JavaScript or TypeScript, open injection profiles and an HTML report after every run. Pick JMeter if you need protocols such as JDBC, LDAP, FTP or mail out of the box, prefer to build tests in a GUI, or already own &lt;code&gt;.jmx&lt;/code&gt; test plans.&lt;/p&gt;
&lt;p&gt;The facts in this Gatling vs JMeter comparison come from each project&amp;#39;s own documentation and repository, checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gatling&lt;/th&gt;
&lt;th&gt;Apache JMeter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Apache 2.0; gRPC, MQTT and GraphQL under a separate component licence (&lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Apache 2.0 (&lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;JVM, OpenJDK LTS 11 to 25 (release 3.16.0)&lt;/td&gt;
&lt;td&gt;JVM, Java 8 or later (release 5.6.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written as&lt;/td&gt;
&lt;td&gt;Code in Java, Kotlin, Scala, JavaScript or TypeScript&lt;/td&gt;
&lt;td&gt;Test plans built in a GUI, saved as XML &lt;code&gt;.jmx&lt;/code&gt; files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in protocols&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, WebSocket, Server-Sent Events, JMS&lt;/td&gt;
&lt;td&gt;HTTP(S), SOAP/REST, FTP, JDBC, LDAP, JMS, mail, TCP, shell commands, Java objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recorder&lt;/td&gt;
&lt;td&gt;HTTP proxy or HAR file converter&lt;/td&gt;
&lt;td&gt;HTTP(S) Test Script Recorder (proxy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;Open or closed injection profiles&lt;/td&gt;
&lt;td&gt;Thread groups; an experimental Open Model Thread Group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;One machine; distributed runs need Gatling Enterprise&lt;/td&gt;
&lt;td&gt;Built-in remote mode, one client driving many servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI and headless runs&lt;/td&gt;
&lt;td&gt;Maven, Gradle, sbt and npm plugins; a failed assertion fails the run&lt;/td&gt;
&lt;td&gt;CLI mode (&lt;code&gt;jmeter -n&lt;/code&gt;); third-party Maven, Gradle and Jenkins plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reports&lt;/td&gt;
&lt;td&gt;HTML report after each run&lt;/td&gt;
&lt;td&gt;HTML dashboard; live results through a Backend Listener&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between Gatling and JMeter?&lt;/h2&gt;
&lt;p&gt;In Gatling a test is a program in a build project. In JMeter a test is a tree of elements you assemble in a desktop GUI and save as an XML file, and most other differences follow from that.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;README&lt;/a&gt; calls the tests simulations and keeps them in your repository like application code, run through a Maven, Gradle, sbt or npm plugin. The &lt;a href=&quot;https://github.com/gatling/gatling-js&quot;&gt;JavaScript and TypeScript SDK&lt;/a&gt; installs through npm and downloads its own GraalVM runtime.&lt;/p&gt;
&lt;p&gt;A JMeter plan holds a thread group for the users, samplers for the requests, and assertions and listeners for the results. JMeter 5.6.3, the current release on the &lt;a href=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot;&gt;download page&lt;/a&gt;, requires Java 8 or later. The README on JMeter&amp;#39;s &lt;a href=&quot;https://github.com/apache/jmeter/blob/master/README.md&quot;&gt;development branch&lt;/a&gt; already calls for Java 17 (checked on 10 October 2026). Underneath, JMeter runs each virtual user as a Java thread, while Gatling&amp;#39;s README describes them as lightweight messages on a non-blocking Netty engine.&lt;/p&gt;
&lt;h2&gt;How do you write a test in each tool?&lt;/h2&gt;
&lt;p&gt;In Gatling you write a simulation class that sets up a scenario, an injection profile and assertions. In JMeter you add a thread group and samplers in the GUI, save the plan, then run it from the command line.&lt;/p&gt;
&lt;p&gt;A Java simulation modelled on Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/tutorials/test-as-code/java-jvm/running-your-first-simulation/&quot;&gt;first simulation tutorial&lt;/a&gt;: new users arrive at one per second, rising to ten per second over a minute and holding there for five minutes. It fails if the 95th percentile tops 500 ms or more than 1% of requests fail:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;

public class ApiSimulation extends Simulation {

  HttpProtocolBuilder httpProtocol =
      http.baseUrl(&amp;quot;https://staging.example.com&amp;quot;);

  ScenarioBuilder users = scenario(&amp;quot;API users&amp;quot;)
      .exec(http(&amp;quot;list users&amp;quot;).get(&amp;quot;/api/users&amp;quot;))
      .pause(1);

  {
    setUp(users.injectOpen(
            rampUsersPerSec(1).to(10).during(60),
            constantUsersPerSec(10).during(300)))
        .protocols(httpProtocol)
        .assertions(
            global().responseTime().percentile(95.0).lt(500),
            global().failedRequests().percent().lt(1.0));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;./mvnw gatling:test&lt;/code&gt;; a JavaScript or TypeScript simulation runs with &lt;code&gt;npx gatling run&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In JMeter the same test is a thread group holding an HTTP Request sampler, and a Duration Assertion can mark each slow response as failed. The &lt;a href=&quot;https://jmeter.apache.org/usermanual/get-started.html&quot;&gt;getting started guide&lt;/a&gt; builds the plan in GUI mode and runs it in CLI mode, with &lt;code&gt;-e -o&lt;/code&gt; adding the HTML report:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;jmeter -n -t api-users.jmx -l results.jtl -e -o report/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both tools can record a first draft from a browser. Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/reference/script/http/recorder/&quot;&gt;Recorder&lt;/a&gt; works as an HTTP proxy or converts a HAR file into a simulation, and JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html&quot;&gt;HTTP(S) Test Script Recorder&lt;/a&gt; captures browser traffic into a plan (checked on 9 October 2026). Gatling&amp;#39;s docs now list its Recorder as legacy, and the newer &lt;a href=&quot;https://docs.gatling.io/tutorials/low-code/browser/studio/&quot;&gt;Gatling Studio&lt;/a&gt; needs an Enterprise account to log in (checked on 10 October 2026).&lt;/p&gt;
&lt;h2&gt;Which protocols can each tool test?&lt;/h2&gt;
&lt;p&gt;JMeter covers more protocols out of the box. Gatling ships with the main web protocols plus JMS, and leaves most of the rest to separate components or community plugins.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;README&lt;/a&gt; lists every protocol in the table above. JDBC and JMS need driver jars from your database or JMS provider, and the &lt;a href=&quot;https://jmeter-plugins.org/install/Install/&quot;&gt;Plugins Manager&lt;/a&gt; adds community plugins from inside the GUI.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s README says gRPC, MQTT and GraphQL are separate components under the Gatling Enterprise Component License, capped at five concurrent users and five-minute tests without Enterprise. Kafka, AMQP, SFTP, FTP and JDBC come from &lt;a href=&quot;https://docs.gatling.io/reference/script/third-parties/&quot;&gt;third-party plugins&lt;/a&gt; for Gatling 3.13 that Gatling does not maintain (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How do Gatling and JMeter model load?&lt;/h2&gt;
&lt;p&gt;Gatling makes you choose between an open and a closed model in the script itself. JMeter&amp;#39;s classic thread group is a closed pool of users, and arrival-based load needs the newer Open Model Thread Group.&lt;/p&gt;
&lt;p&gt;In Gatling, &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;&lt;code&gt;injectOpen&lt;/code&gt;&lt;/a&gt; takes arrival rates such as &lt;code&gt;constantUsersPerSec&lt;/code&gt; and &lt;code&gt;rampUsersPerSec&lt;/code&gt;, and &lt;code&gt;injectClosed&lt;/code&gt; takes concurrency targets such as &lt;code&gt;constantConcurrentUsers&lt;/code&gt;. JMeter 5.5 added the &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Open_Model_Thread_Group&quot;&gt;Open Model Thread Group&lt;/a&gt;, which the reference still marks as experimental (checked on 9 October 2026). Our guide to the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains why that choice matters.&lt;/p&gt;
&lt;h2&gt;How do you run a test across several machines?&lt;/h2&gt;
&lt;p&gt;JMeter has a distributed mode built in. Gatling&amp;#39;s open source edition generates load from one machine, and its docs list a Gatling Enterprise account as a requirement for distributed runs.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/remote-test.html&quot;&gt;remote testing mode&lt;/a&gt; lets one client control any number of servers. Each server runs the whole plan, so 1,000 threads on six servers means 6,000 threads. All nodes should run the same JMeter and Java versions, RMI needs a keystore unless SSL is disabled, and data files must be copied to each server.&lt;/p&gt;
&lt;p&gt;There is a third route for JMeter test plans. LoadFocus, which publishes this site, &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gatling-vs-jmeter&quot;&gt;runs existing JMeter test plans&lt;/a&gt; unchanged from 25 cloud regions, with no remote servers to set up. LoadFocus does not run Gatling simulations.&lt;/p&gt;
&lt;h2&gt;How do they fit into CI, and what reports do you get?&lt;/h2&gt;
&lt;p&gt;Both run headless. Gatling&amp;#39;s assertions can fail the build directly, while JMeter&amp;#39;s assertions flag individual samples and CI support comes from third-party libraries.&lt;/p&gt;
&lt;p&gt;Gatling &lt;a href=&quot;https://docs.gatling.io/concepts/assertions/&quot;&gt;assertions&lt;/a&gt; are checked after the run, and one failed assertion fails the simulation and the build task. The &lt;a href=&quot;https://docs.gatling.io/reference/stats/reports/oss/&quot;&gt;HTML report&lt;/a&gt; shows response time percentiles, users started per second, concurrent users and requests per second over time.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s CLI mode has no pass/fail option, so a missed target fails the build only through the Maven, Gradle and Jenkins libraries its README lists, or your own check. JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/generating-dashboard.html&quot;&gt;dashboard&lt;/a&gt; adds an APDEX table, a statistics table with three configurable percentiles and error summaries, and a Backend Listener streams live results to InfluxDB or Graphite.&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Choose Gatling when your team writes Java, Kotlin, Scala or TypeScript and wants load tests reviewed like the rest of its code. Choose JMeter instead when you need protocols beyond the web, when your testers prefer a GUI, or when you already have &lt;code&gt;.jmx&lt;/code&gt; plans.&lt;/p&gt;
&lt;p&gt;Gatling suits teams that describe traffic as arrival rates, want pass/fail rules next to the scenario and a readable report with no extra setup.&lt;/p&gt;
&lt;p&gt;JMeter is the better choice for JDBC, LDAP, FTP or mail testing, for testers who refine a recorded flow visually, and for distributed runs on your own servers without a commercial licence. A company that already runs JMeter also keeps its plans, plugins and know-how.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Gatling faster than JMeter?&lt;/strong&gt;
We have not measured this ourselves, so we will not name a winner. The fair test is the same scenario with both tools on your own hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Gatling run JMeter .jmx files?&lt;/strong&gt;
Not directly. Gatling runs simulations written with its SDKs, so a JMeter plan has to be rewritten. Gatling&amp;#39;s README lists AI extensions that convert JMeter test plans and require a Gatling Enterprise API token.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Gatling tests?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To spread a Gatling simulation across several machines, use Gatling Enterprise.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling load testing: a review of the JVM tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6: JVM simulations or a single Go binary?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter: Python scripts or JMeter test plans?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-locust/&quot;&gt;Gatling vs Locust: JVM simulations or Python locustfiles?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/jmeter-vs-postman/&quot;&gt;JMeter vs Postman: which tool for load testing?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Coordinated omission: what it is and how to avoid it</title><link>https://loadtest.app/coordinated-omission/</link><guid isPermaLink="true">https://loadtest.app/coordinated-omission/</guid><description>Coordinated omission explained: why a load generator that waits for slow replies hides them, how to spot it and how wrk2, k6, Gatling and JMeter avoid it.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Coordinated omission is a measurement error in which a load generator waits for each slow response before sending its next request, so it sends fewer requests just when the system is slow and records too few of the bad moments. The percentiles you read afterwards then look better than what users lived through. You avoid it by sending requests on a schedule that ignores response times, and by timing each request from the moment it was due to be sent.&lt;/p&gt;
&lt;p&gt;The tool facts below come from each project&amp;#39;s own documentation or repository, checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it offers&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;wrk2&lt;/td&gt;
&lt;td&gt;A constant rate (&lt;code&gt;-R&lt;/code&gt;), latency timed from the planned send&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--u_latency&lt;/code&gt; adds the uncorrected view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HdrHistogram&lt;/td&gt;
&lt;td&gt;Two methods that back-fill missing samples&lt;/td&gt;
&lt;td&gt;Apply one of them, never both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;Arrival-rate executors&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;dropped_iterations&lt;/code&gt; counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;&lt;code&gt;injectOpen&lt;/code&gt; steps such as &lt;code&gt;constantUsersPerSec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The steps sit inside &lt;code&gt;injectOpen&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;&lt;code&gt;constant_throughput&lt;/code&gt; paces each user&lt;/td&gt;
&lt;td&gt;The achieved rate against the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMeter&lt;/td&gt;
&lt;td&gt;Open Model Thread Group (5.5+, experimental)&lt;/td&gt;
&lt;td&gt;A plain Thread Group is a closed pool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is coordinated omission?&lt;/h2&gt;
&lt;p&gt;It is the error you get when the measuring side stops taking samples because the system is slow, so the samples that go missing are exactly the slow ones. Losing samples at random barely moves a percentile; losing them in step with a slowdown skews all of them.&lt;/p&gt;
&lt;p&gt;The name comes from Gil Tene, whose &lt;a href=&quot;https://groups.google.com/g/mechanical-sympathy/c/icNZJejUHfE&quot;&gt;2013 mailing list post&lt;/a&gt; on the problem draws this same line. He is the original author of &lt;a href=&quot;https://hdrhistogram.github.io/HdrHistogram/&quot;&gt;HdrHistogram&lt;/a&gt;, wrk2 lives in his GitHub account, and both projects&amp;#39; docs explain the effect (checked on 9 October 2026). The k6 docs use the term for a closed workload model that slows its own load down, a common cause; our &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; article covers that choice, so this one stays with the measurement.&lt;/p&gt;
&lt;h2&gt;How does a load test end up hiding slow responses?&lt;/h2&gt;
&lt;p&gt;A closed-loop generator sends the next request on a connection only after the previous response arrives. When one response takes a second, that connection sends nothing for a second, and the requests it would have sent never get a response time.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/giltene/wrk2&quot;&gt;wrk2 README&lt;/a&gt; says this of wrk&amp;#39;s model, which times each request from first byte sent to full response: since a connection only sends after a response, the generator coordinates with the server and avoids measuring during high-latency periods (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/HdrHistogram/HdrHistogram&quot;&gt;HdrHistogram&amp;#39;s README&lt;/a&gt; offers a thought experiment. A system sampled every 10 ms answers in 1 ms for 100 seconds, then stalls for 100 seconds, yielding one 100-second sample. The raw histogram shows about 99.99% of results at 1 ms or below; corrected for the blocked samples, only about half are.&lt;/p&gt;
&lt;h2&gt;How can you tell if your results have coordinated omission?&lt;/h2&gt;
&lt;p&gt;Compare the rate you sent with the rate you planned. If the request rate fell while response times rose, the generator backed off and the slow period is under-sampled.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The load was set as users or connections with no rate.&lt;/strong&gt; Any closed loop can do this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The request rate dips where response times climb.&lt;/strong&gt; A dip in the rate during a spike means requests went unsent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The maximum dwarfs the 99th percentile.&lt;/strong&gt; A few huge samples among thousands of fast ones is the example above in miniature. Read the whole &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/response-time-distribution?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=coordinated-omission&quot;&gt;response time distribution&lt;/a&gt;, knowing it only counts requests that were sent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The load generator was short of CPU.&lt;/strong&gt; LoadFocus documents that an engine near 100% CPU cannot send at the configured rate, and its results show a &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/load-generator-saturation?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=coordinated-omission&quot;&gt;load generator saturation&lt;/a&gt; warning when an engine peaked at or above 80% CPU.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How does wrk2 avoid coordinated omission?&lt;/h2&gt;
&lt;p&gt;It sends at a constant rate and times each request from when the plan said it should go out. A request that leaves late because its connection was busy is charged for the wait.&lt;/p&gt;
&lt;p&gt;wrk2 adds a rate argument (&lt;code&gt;-R&lt;/code&gt;, total requests per second) to wrk and records latencies in an HdrHistogram. &lt;code&gt;--u_latency&lt;/code&gt; adds the uncorrected histogram, labelled CO, to the report, as in the README&amp;#39;s own example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wrk -t2 -c100 -d30s -R2000 --u_latency http://127.0.0.1:80/index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the README&amp;#39;s second example, a 1.4-second server pause during a 30-second run made the corrected 99th percentile 200 times the uncorrected one. The README also calls the project experimental and warns that runs under 10 to 20 seconds may not be useful (checked on 9 October 2026). Our &lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools review&lt;/a&gt; compares wrk2 with hey, oha and others.&lt;/p&gt;
&lt;h2&gt;How does HdrHistogram correct recorded latencies?&lt;/h2&gt;
&lt;p&gt;If you know the interval at which requests were meant to go out, it adds the samples that a long response blocked. You can correct while recording or afterwards, on a copy.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;recordValueWithExpectedInterval&lt;/code&gt; records a value and, when it exceeds the interval, adds values stepping down by that interval. &lt;code&gt;copyCorrectedForCoordinatedOmission&lt;/code&gt; returns a corrected copy of a finished histogram. The &lt;a href=&quot;https://hdrhistogram.github.io/HdrHistogram/JavaDoc/org/HdrHistogram/AbstractHistogram.html&quot;&gt;JavaDoc&lt;/a&gt; calls the two mutually exclusive (checked on 9 October 2026).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// planned interval: 10 ms, values in microseconds
Histogram h = new Histogram(3);
h.recordValueWithExpectedInterval(latencyUs, 10_000);

// or, after the run, on a raw histogram:
Histogram fixed = raw.copyCorrectedForCoordinatedOmission(10_000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The correction is only as good as the interval you pass in, so it suits a generator that was meant to send at a steady pace.&lt;/p&gt;
&lt;h2&gt;How do k6, Gatling, Locust and JMeter handle it?&lt;/h2&gt;
&lt;p&gt;k6, Gatling and JMeter can start work on a schedule that does not wait for responses. Locust paces each user but cannot start extra users to hold a rate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;k6.&lt;/strong&gt; In the arrival-rate executors, &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate/&quot;&gt;iterations start independently of system response&lt;/a&gt; while a virtual user is free. When none is, k6 skips the iteration and counts it in &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/dropped-iterations/&quot;&gt;&lt;code&gt;dropped_iterations&lt;/code&gt;&lt;/a&gt;, which late in a test may mean the system is degrading (checked on 9 October 2026). A dropped iteration has no response time, so read that counter next to the percentiles. On LoadFocus, &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=coordinated-omission&quot;&gt;k6 scripts&lt;/a&gt; run arrival-rate scenarios with a fixed number of users, a closed model, so run k6 yourself when the arrival rate is the point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gatling.&lt;/strong&gt; Steps such as &lt;code&gt;constantUsersPerSec&lt;/code&gt; inside &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;&lt;code&gt;injectOpen&lt;/code&gt;&lt;/a&gt; start new virtual users at a set rate, whether or not earlier ones have finished (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locust.&lt;/strong&gt; &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;&lt;code&gt;constant_throughput&lt;/code&gt;&lt;/a&gt; adapts each user&amp;#39;s wait so it runs a task at most a set number of times per second. The docs say wait time can only limit the rate and cannot launch new users, and &lt;a href=&quot;https://docs.locust.io/en/stable/api.html&quot;&gt;the API reference&lt;/a&gt; adds that a task which overruns its interval gets no wait afterwards, so a slow response still delays that user (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JMeter.&lt;/strong&gt; A &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Thread_Group&quot;&gt;Thread Group&lt;/a&gt; is a pool of looping threads, a closed loop. JMeter 5.5 added the experimental &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Open_Model_Thread_Group&quot;&gt;Open Model Thread Group&lt;/a&gt;, which follows a schedule such as &lt;code&gt;rate(1/sec) random_arrivals(2 min)&lt;/code&gt; and, per the &lt;a href=&quot;https://jmeter.apache.org/changes_history.html&quot;&gt;release notes&lt;/a&gt;, spawns threads as needed (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;Does an open workload model remove coordinated omission?&lt;/h2&gt;
&lt;p&gt;It removes the main cause, a generator that waits before sending. Two gaps remain: a generator short of users or CPU still falls behind its schedule, and each user in a multi-step journey still waits for its own responses.&lt;/p&gt;
&lt;p&gt;The first gap shows up as dropped iterations or a saturated engine; either is a reason to rerun with more headroom. The second is built into most HTTP testing: the wrk2 README notes that sending without waiting only suits non-blocking protocols or one request per connection, which is why wrk2 corrects the timing instead.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Who named coordinated omission?&lt;/strong&gt;
Gil Tene, the original author of HdrHistogram, whose wrk2 fork of wrk is built around avoiding it. His 2013 post to the mechanical-sympathy mailing list describes it as a problem he calls by that name.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is coordinated omission the same as a closed workload model?&lt;/strong&gt;
They are related. A closed model is a common cause, since each virtual user waits for its response before sending again, and the wrk2 README also finds the effect in monitoring code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should you correct every latency histogram for it?&lt;/strong&gt;
Only when you know the interval at which requests were planned, because HdrHistogram needs it to fill in the missing samples. Apply one of its two correction methods to a data set, never both.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools: wrk, hey, oha, ab and more&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/ycsb/&quot;&gt;YCSB: workloads A-F, how to run it and its limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>How to load test a GraphQL API: a practical guide</title><link>https://loadtest.app/how-to-load-test-a-graphql-api/</link><guid isPermaLink="true">https://loadtest.app/how-to-load-test-a-graphql-api/</guid><description>How to load test a GraphQL API: name every operation, mix queries and mutations, catch the errors hidden behind HTTP 200 and watch caching and N+1.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;To load test a GraphQL API, script the operations your clients really send, give each one its own name in the test tool and mix them in the proportions you see in production, with varied variables. Check every response body for an &lt;code&gt;errors&lt;/code&gt; array, because a GraphQL server usually answers with HTTP 200 even when part of an operation failed. Then ramp up the load and judge each operation by its own percentiles.&lt;/p&gt;
&lt;p&gt;Sources: graphql.org, the draft GraphQL over HTTP specification and each tool&amp;#39;s own documentation, all checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Name each operation with&lt;/th&gt;
&lt;th&gt;Fail a response that has &lt;code&gt;errors&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Feed variables from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tags: { name: &amp;quot;...&amp;quot; }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;check()&lt;/code&gt; on the parsed body&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SharedArray&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMeter&lt;/td&gt;
&lt;td&gt;The sampler name&lt;/td&gt;
&lt;td&gt;JSON Assertion on &lt;code&gt;$.errors&lt;/code&gt;, inverted&lt;/td&gt;
&lt;td&gt;CSV Data Set Config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http(&amp;quot;...&amp;quot;)&lt;/code&gt; or the GraphQL plugin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jsonPath(&amp;quot;$.errors&amp;quot;).notExists()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Feeders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artillery&lt;/td&gt;
&lt;td&gt;A request &lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;notHasProperty: &amp;quot;errors&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config.payload&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Why does a GraphQL API need a different load test?&lt;/h2&gt;
&lt;p&gt;Because every operation goes to one URL, and the work behind it depends on the query and its variables. A report grouped by URL puts a cheap lookup and an expensive search in the same bucket.&lt;/p&gt;
&lt;p&gt;graphql.org&amp;#39;s &lt;a href=&quot;https://graphql.org/learn/serving-over-http/&quot;&gt;Serving over HTTP&lt;/a&gt; guide says a GraphQL server operates on a single endpoint, usually &lt;code&gt;/graphql&lt;/code&gt;. Operations arrive there by POST with a JSON body holding &lt;code&gt;query&lt;/code&gt;, an optional &lt;code&gt;operationName&lt;/code&gt; and &lt;code&gt;variables&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;How do you get results per operation?&lt;/h2&gt;
&lt;p&gt;Name every request after its GraphQL operation and make the tool group its statistics by that name. Each tool has its own switch for this (checked on 9 October 2026).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;k6&lt;/strong&gt;: the &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/tags-and-groups/&quot;&gt;tags and groups guide&lt;/a&gt; says to set &lt;code&gt;tags.name&lt;/code&gt; to give a request a meaningful name, and &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt; can target one tag, such as &lt;code&gt;http_req_duration{name:GetProduct}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JMeter&lt;/strong&gt;: the &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Aggregate_Report&quot;&gt;Aggregate Report&lt;/a&gt; creates one row per differently named request. Since JMeter 5.4 the &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#HTTP_Request&quot;&gt;GraphQL HTTP Request&lt;/a&gt; sampler has Query, Variables and Operation Name fields.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gatling&lt;/strong&gt;: the &lt;a href=&quot;https://docs.gatling.io/reference/script/http/request/&quot;&gt;request name&lt;/a&gt; is the key for its statistics, and the &lt;a href=&quot;https://docs.gatling.io/reference/script/graphql/requests/&quot;&gt;GraphQL plugin&lt;/a&gt; names requests after the operation, such as &lt;code&gt;query GetUser&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Artillery&lt;/strong&gt;: the &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/metrics-by-endpoint&quot;&gt;metrics-by-endpoint plugin&lt;/a&gt; reports per URL and adds a request&amp;#39;s &lt;code&gt;name&lt;/code&gt; to its entry, so named operations are reported separately; &lt;code&gt;useOnlyRequestNames: true&lt;/code&gt; drops the URL and keeps the name alone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Which operations should the test send?&lt;/h2&gt;
&lt;p&gt;The ones production receives, in the same proportions. Take the operation counts from your server logs or GraphQL monitoring and turn them into a weighted mix of reads and mutations.&lt;/p&gt;
&lt;p&gt;Use the documents your clients ship, at their real depth, rather than one simplified query. Feed variables from a data file, so the server looks up many different records instead of returning one cached result. Keep the number of distinct operations close to reality too: Apollo&amp;#39;s &lt;a href=&quot;https://www.apollographql.com/docs/technotes/TN0035-load-testing-graphql&quot;&gt;technote on load testing GraphQL&lt;/a&gt;, a vendor document, explains that each new operation is parsed and validated before it runs, and warns against generating far more unique operations than real clients send.&lt;/p&gt;
&lt;p&gt;Mutations change data, so run them where the data can be reset. The &lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;step-by-step load testing guide&lt;/a&gt; covers goals and ramp-up.&lt;/p&gt;
&lt;h2&gt;Why can an HTTP 200 response still be a failure?&lt;/h2&gt;
&lt;p&gt;Because GraphQL reports errors inside the response body. A request can return status 200 with an &lt;code&gt;errors&lt;/code&gt; array and partial data, and a status check passes it.&lt;/p&gt;
&lt;p&gt;graphql.org&amp;#39;s &lt;a href=&quot;https://graphql.org/learn/response/&quot;&gt;Response&lt;/a&gt; page explains that when a resolver fails, execution continues and the response carries &lt;code&gt;data&lt;/code&gt; alongside &lt;code&gt;errors&lt;/code&gt;. Its HTTP guide adds that such a response should still get a 2xx status, since HTTP has no code for partial success, and that some legacy servers answer 2xx even for validation errors. The &lt;a href=&quot;https://graphql.github.io/graphql-over-http/draft/&quot;&gt;GraphQL over HTTP draft&lt;/a&gt; proposes a custom 294 status for partial success with the newer &lt;code&gt;application/graphql-response+json&lt;/code&gt; media type.&lt;/p&gt;
&lt;p&gt;In a load test this hurts twice. Errors go uncounted, and requests that fail fast, such as operations rejected by a depth limit, pull the response times down.&lt;/p&gt;
&lt;h2&gt;How do you check the errors array in each tool?&lt;/h2&gt;
&lt;p&gt;Parse the JSON body and fail the request when &lt;code&gt;errors&lt;/code&gt; is present. Each tool has a built-in way.&lt;/p&gt;
&lt;p&gt;In k6, a check on the parsed body does it, and a threshold on the &lt;code&gt;checks&lt;/code&gt; metric fails the run, because &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;checks alone do not change k6&amp;#39;s exit code&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;
import { check, sleep } from &amp;quot;k6&amp;quot;;
import { SharedArray } from &amp;quot;k6/data&amp;quot;;

const URL = &amp;quot;https://staging.example.com/graphql&amp;quot;;
const ids = new SharedArray(&amp;quot;ids&amp;quot;, () =&amp;gt;
  JSON.parse(open(&amp;quot;./product-ids.json&amp;quot;)));
const QUERY = `query GetProduct($id: ID!) {
  product(id: $id) { id name price }
}`;

export const options = {
  vus: 20,
  duration: &amp;quot;5m&amp;quot;,
  thresholds: {
    checks: [&amp;quot;rate&amp;gt;0.99&amp;quot;],
    &amp;quot;http_req_duration{name:GetProduct}&amp;quot;: [&amp;quot;p(95)&amp;lt;400&amp;quot;],
  },
};

function parse(res) {
  try { return res.json(); } catch (e) { return null; }
}

export default function () {
  const id = ids[Math.floor(Math.random() * ids.length)];
  const payload = JSON.stringify({
    query: QUERY,
    operationName: &amp;quot;GetProduct&amp;quot;,
    variables: { id: id },
  });
  const res = http.post(URL, payload, {
    headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot; },
    tags: { name: &amp;quot;GetProduct&amp;quot; },
  });
  const body = parse(res);
  check(res, {
    &amp;quot;status is 2xx&amp;quot;: (r) =&amp;gt; r.status &amp;gt;= 200 &amp;amp;&amp;amp; r.status &amp;lt; 300,
    &amp;quot;no GraphQL errors&amp;quot;: () =&amp;gt; body !== null &amp;amp;&amp;amp; !body.errors,
  });
  sleep(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6-data/sharedarray/&quot;&gt;&lt;code&gt;SharedArray&lt;/code&gt;&lt;/a&gt; loads the IDs once for all virtual users. The &lt;code&gt;try&lt;/code&gt; stops a body that is not JSON, such as a proxy&amp;#39;s error page, from ending the iteration.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;JMeter&lt;/strong&gt;, add a &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#JSON_Assertion&quot;&gt;JSON Assertion&lt;/a&gt; with the path &lt;code&gt;$.errors&lt;/code&gt; and tick &amp;quot;Invert assertion&amp;quot;, so the sample fails when the path exists. A &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#JSON_Extractor&quot;&gt;JSON Extractor&lt;/a&gt; copies IDs into variables for the next operation.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Gatling&lt;/strong&gt;, add &lt;code&gt;.check(jsonPath(&amp;quot;$.errors&amp;quot;).notExists())&lt;/code&gt;, both from its &lt;a href=&quot;https://docs.gatling.io/concepts/checks/&quot;&gt;checks reference&lt;/a&gt;. The GraphQL plugin checks the status and the errors by default, but its &lt;a href=&quot;https://docs.gatling.io/reference/script/graphql/setup/&quot;&gt;setup page&lt;/a&gt; limits Community Edition runs to 5 users and 5 minutes (checked on 9 October 2026). For bigger tests without Gatling Enterprise, use plain HTTP requests with the check.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Artillery&lt;/strong&gt;, the &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/expect&quot;&gt;expect plugin&lt;/a&gt; offers &lt;code&gt;notHasProperty: &amp;quot;errors&amp;quot;&lt;/code&gt;, and &lt;code&gt;reportFailuresAsErrors: true&lt;/code&gt; puts failed expectations in the report. A &lt;a href=&quot;https://www.artillery.io/docs/reference/engines/http&quot;&gt;&lt;code&gt;capture&lt;/code&gt;&lt;/a&gt; with a &lt;code&gt;json&lt;/code&gt; path carries a value into the next request.&lt;/p&gt;
&lt;h2&gt;What should you watch besides response times?&lt;/h2&gt;
&lt;p&gt;Caching, persisted queries and database load per request. Each can make a test faster or slower than production for reasons unrelated to capacity.&lt;/p&gt;
&lt;p&gt;graphql.org&amp;#39;s &lt;a href=&quot;https://graphql.org/learn/performance/&quot;&gt;performance guide&lt;/a&gt; notes that queries sent by GET can use HTTP and CDN caching, and that persisted queries let a client send a hash instead of the full document. If your clients do that, the test should too. If they do not, a test repeating one query with the same variables measures a cache that real traffic misses. Apollo&amp;#39;s technote adds that Apollo Server, Gateway and the GraphOS Router enable automatic persisted queries by default.&lt;/p&gt;
&lt;p&gt;The same guide describes the N+1 problem: a list whose resolver loads each item separately turns one request into many database queries, usually fixed by batching with a tool like DataLoader. Watch database queries per second next to requests per second; if they climb much faster as the load rises, suspect a resolver. Depth limits, complexity limits and rate limiting can also trip during a test.&lt;/p&gt;
&lt;h2&gt;Where should the load come from?&lt;/h2&gt;
&lt;p&gt;From one machine while you build and smoke test the script, then from the cloud when you need more load or traffic from several regions.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=how-to-load-test-a-graphql-api&quot;&gt;LoadFocus runs k6 scripts&lt;/a&gt; from 25 cloud regions. There, the number of users and the duration come from the test settings rather than the script&amp;#39;s &lt;code&gt;options&lt;/code&gt;, and arrival-rate scenarios run with a fixed number of users instead of a fixed request rate, the difference covered in &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload models&lt;/a&gt;. LoadFocus also publishes a &lt;a href=&quot;https://loadfocus.com/templates/load-testing-for-graphql-endpoints-with-complex-queries?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=how-to-load-test-a-graphql-api&quot;&gt;template for GraphQL endpoints with complex queries&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Should a GraphQL load test send queries by GET or POST?&lt;/strong&gt;
Send them the way your clients do. graphql.org allows GET for queries only, and mutations must use POST. If production sends persisted queries by GET through a CDN, test that path too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can the same script load test GraphQL subscriptions?&lt;/strong&gt;
Usually not. graphql.org notes that subscriptions are long-lived and often run over WebSockets or server-sent events, so they need their own scenario in a tool that supports that transport.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should a partial response count as a failed request?&lt;/strong&gt;
In most tests, yes, because the client got an error for part of its request. If some errors are expected, such as not-found results for deleted records, match those error codes and fail on the rest.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;Load testing in GitHub Actions: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools: six options compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/grpc-load-testing/&quot;&gt;gRPC load testing: tools, setup and what to watch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Gatling vs k6: JVM simulations or a single Go binary?</title><link>https://loadtest.app/gatling-vs-k6/</link><guid isPermaLink="true">https://loadtest.app/gatling-vs-k6/</guid><description>Gatling vs k6 compared on licence, runtime, scripting languages, protocols, load models, pass/fail checks and scaling out, with advice on when each fits.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Gatling and k6 are both open source tools that keep load tests as code in your repository, and either one will load test an HTTP API well. Pick Gatling if your team works in Java, Kotlin or Scala, or wants an HTML report after every run with no extra setup. Pick k6 if you want one Go binary with no JVM, gRPC and browser tests out of the box, and thresholds that can stop a failing run early.&lt;/p&gt;
&lt;p&gt;The facts in this Gatling vs k6 comparison come from each project&amp;#39;s own documentation and repository, checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gatling&lt;/th&gt;
&lt;th&gt;k6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Apache 2.0; gRPC, MQTT and GraphQL under a separate component licence (&lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;AGPL-3.0 (&lt;a href=&quot;https://github.com/grafana/k6&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;JVM; the Java SDK needs OpenJDK LTS 11 to 25 (release 3.16.0)&lt;/td&gt;
&lt;td&gt;One Go binary with an embedded JavaScript engine (release v2.3.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test scripts&lt;/td&gt;
&lt;td&gt;Java, Kotlin, Scala, JavaScript or TypeScript&lt;/td&gt;
&lt;td&gt;JavaScript, or TypeScript with the types stripped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core protocols&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, WebSocket, Server-Sent Events, JMS&lt;/td&gt;
&lt;td&gt;HTTP/1.1, HTTP/2, WebSockets, gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load model&lt;/td&gt;
&lt;td&gt;Open or closed injection profiles&lt;/td&gt;
&lt;td&gt;Six executors, two of them arrival-rate (open model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass/fail&lt;/td&gt;
&lt;td&gt;Assertions, checked after the run&lt;/td&gt;
&lt;td&gt;Thresholds, which can also abort a run early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reports&lt;/td&gt;
&lt;td&gt;HTML report after each run&lt;/td&gt;
&lt;td&gt;End-of-test summary; web dashboard with HTML export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser testing&lt;/td&gt;
&lt;td&gt;None built in; the Recorder turns browser traffic into code&lt;/td&gt;
&lt;td&gt;Built-in browser module (drives a Chromium-based browser)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;Gatling Enterprise&lt;/td&gt;
&lt;td&gt;k6-operator on Kubernetes, or Grafana Cloud k6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between Gatling and k6?&lt;/h2&gt;
&lt;p&gt;The biggest difference is the runtime. Gatling runs on the JVM and you write tests with one of its SDKs, while k6 is a single compiled Go program that runs JavaScript.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;README&lt;/a&gt; lists a Java SDK for Java, Kotlin and Scala, run through a Maven, Gradle or sbt plugin. Its &lt;a href=&quot;https://github.com/gatling/gatling-js&quot;&gt;JavaScript and TypeScript SDK&lt;/a&gt; installs through npm, and its command line tool downloads GraalVM, so even a JavaScript simulation runs on a JVM.&lt;/p&gt;
&lt;p&gt;k6 installs as &lt;a href=&quot;https://grafana.com/docs/k6/latest/set-up/install-k6/&quot;&gt;a package or a standalone binary&lt;/a&gt; with no other runtime. It runs &lt;code&gt;.ts&lt;/code&gt; files directly too, though its &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/javascript-typescript-compatibility-mode/&quot;&gt;TypeScript support&lt;/a&gt; strips the types without checking them (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you write the same test in each tool?&lt;/h2&gt;
&lt;p&gt;Both tools can express the same test in JavaScript. Gatling calls it a simulation and describes load as an injection profile, while k6 calls it a script and describes load as a scenario with an executor.&lt;/p&gt;
&lt;p&gt;Here is a test where ten new users arrive every second for five minutes, and the run fails if the 95th percentile tops 500 ms or more than 1% of requests fail. In Gatling&amp;#39;s JavaScript SDK, modelled on its &lt;a href=&quot;https://docs.gatling.io/tutorials/test-as-code/javascript/running-your-first-simulation/&quot;&gt;first simulation tutorial&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import {
  constantUsersPerSec, global, scenario, simulation,
} from &amp;quot;@gatling.io/core&amp;quot;;
import { http } from &amp;quot;@gatling.io/http&amp;quot;;

export default simulation((setUp) =&amp;gt; {
  const api = http.baseUrl(&amp;quot;https://staging.example.com&amp;quot;);

  const users = scenario(&amp;quot;API users&amp;quot;)
    .exec(http(&amp;quot;list users&amp;quot;).get(&amp;quot;/api/users&amp;quot;));

  setUp(users.injectOpen(constantUsersPerSec(10).during(300)))
    .protocols(api)
    .assertions(
      global().responseTime().percentile(95).lt(500),
      global().failedRequests().percent().lt(1),
    );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same test in k6, with the &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate/&quot;&gt;constant-arrival-rate executor&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;

export const options = {
  scenarios: {
    api_users: {
      executor: &amp;quot;constant-arrival-rate&amp;quot;,
      rate: 10,
      timeUnit: &amp;quot;1s&amp;quot;,
      duration: &amp;quot;5m&amp;quot;,
      preAllocatedVUs: 50,
    },
  },
  thresholds: {
    http_req_duration: [&amp;quot;p(95)&amp;lt;500&amp;quot;],
    http_req_failed: [&amp;quot;rate&amp;lt;0.01&amp;quot;],
  },
};

export default function () {
  http.get(&amp;quot;https://staging.example.com/api/users&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run them with &lt;code&gt;npx gatling run&lt;/code&gt; and &lt;code&gt;k6 run script.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;How do Gatling and k6 model load?&lt;/h2&gt;
&lt;p&gt;Both support open and closed workload models. Gatling makes the choice through the injection method you call, and k6 through the executor you pick.&lt;/p&gt;
&lt;p&gt;In Gatling, &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;&lt;code&gt;injectOpen&lt;/code&gt;&lt;/a&gt; takes arrival rates such as &lt;code&gt;constantUsersPerSec&lt;/code&gt;, and &lt;code&gt;injectClosed&lt;/code&gt; takes concurrency targets such as &lt;code&gt;constantConcurrentUsers&lt;/code&gt;. k6 has &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/&quot;&gt;six executors&lt;/a&gt;: the two arrival-rate ones follow the open model, and the other four follow the closed model, where a virtual user starts its next iteration only when the last one finishes.&lt;/p&gt;
&lt;p&gt;A k6 arrival-rate scenario starts iterations from a pool of pre-allocated virtual users, and when the pool runs dry, k6 skips iterations and counts them as &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/dropped-iterations/&quot;&gt;dropped iterations&lt;/a&gt; (checked on 9 October 2026). Our guide to the &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; explains when each model fits.&lt;/p&gt;
&lt;h2&gt;How do Gatling and k6 decide whether a test passed?&lt;/h2&gt;
&lt;p&gt;Both turn performance goals into a pass or fail result that a CI pipeline can act on. Gatling checks its goals after the run ends, while k6 can also stop a run as soon as a goal is missed.&lt;/p&gt;
&lt;p&gt;Gatling &lt;a href=&quot;https://docs.gatling.io/concepts/assertions/&quot;&gt;assertions&lt;/a&gt; are evaluated after the simulation, and one failed assertion fails the simulation and the build. k6 &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt; end a failed run with a non-zero exit code, and a threshold set with &lt;code&gt;abortOnFail&lt;/code&gt; stops the test the moment it fails (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;Which protocols and browser tests does each tool cover?&lt;/h2&gt;
&lt;p&gt;Both cover HTTP and WebSockets out of the box. Gatling adds Server-Sent Events and JMS, and k6 adds gRPC and real browser sessions.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s README says gRPC, MQTT and GraphQL are separate components under the Gatling Enterprise Component License, capped at five concurrent users and five-minute tests without Enterprise. k6 lists HTTP/1.1, HTTP/2, WebSockets and gRPC as &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/protocols/&quot;&gt;built-in protocols&lt;/a&gt;, with more through xk6 extensions such as Kafka and Redis.&lt;/p&gt;
&lt;p&gt;For browser tests, k6 has a &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6-browser/&quot;&gt;browser module&lt;/a&gt; that drives a Chromium-based browser and collects frontend metrics. Gatling&amp;#39;s docs describe no browser engine: the &lt;a href=&quot;https://docs.gatling.io/reference/script/http/recorder/&quot;&gt;Recorder&lt;/a&gt; turns a browser session or HAR file into an HTTP-level simulation (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you run a bigger test than one machine can generate?&lt;/h2&gt;
&lt;p&gt;Gatling&amp;#39;s open source edition generates load from one machine, and distributed runs need Gatling Enterprise. k6 can spread a test over a Kubernetes cluster with the open source k6-operator, or run it on Grafana Cloud k6.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/tutorials/test-as-code/java-jvm/installation-guide/&quot;&gt;installation guide&lt;/a&gt; lists an Enterprise account for distributed runs, which use managed load generators or private locations in your own AWS, Azure, GCP or Kubernetes account. The &lt;a href=&quot;https://grafana.com/docs/k6/latest/testing-guides/running-distributed-tests/&quot;&gt;k6-operator&lt;/a&gt; splits one test across several k6 instances in a cluster (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;There is a third route for k6 scripts. LoadFocus, which publishes this site, &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gatling-vs-k6&quot;&gt;runs k6 scripts&lt;/a&gt; on its own cloud infrastructure from 25 regions, with the users and duration taken from the test&amp;#39;s settings. Its script validator warns that arrival-rate scenarios run there with a fixed number of users, so an open-model script like the one above does not hold its arrival rate on LoadFocus. LoadFocus does not run Gatling simulations, so for Gatling the route to more machines is Gatling Enterprise.&lt;/p&gt;
&lt;h2&gt;What reports do you get?&lt;/h2&gt;
&lt;p&gt;Gatling writes an HTML report after every run with no setup. k6 prints a summary in the terminal and can produce an HTML report from its built-in web dashboard.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/reference/stats/reports/oss/&quot;&gt;HTML report&lt;/a&gt; charts response time percentiles, users started per second and requests per second over time. k6 prints an &lt;a href=&quot;https://grafana.com/docs/k6/latest/results-output/end-of-test/&quot;&gt;end-of-test summary&lt;/a&gt;, and its &lt;a href=&quot;https://grafana.com/docs/k6/latest/results-output/web-dashboard/&quot;&gt;web dashboard&lt;/a&gt; shows live charts and exports a self-contained HTML report when you set &lt;code&gt;K6_WEB_DASHBOARD_EXPORT&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Gatling fits teams on the JVM, and k6 fits teams that want one binary and JavaScript. Protocols, browser tests and scaling out settle most other cases.&lt;/p&gt;
&lt;p&gt;Choose Gatling instead of k6 when your team lives on the JVM and wants Java, Kotlin or Scala tests built with Maven, Gradle or sbt next to the application code, when you need JMS or Server-Sent Events, or when you want an HTML report after every run with no setup.&lt;/p&gt;
&lt;p&gt;Choose k6 instead of Gatling when you want a single binary in CI with no JVM to install, when your tests need gRPC or real browser sessions, when thresholds should stop a failing run early, or when you plan to scale out on Kubernetes with an open source operator.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Gatling faster than k6?&lt;/strong&gt;
We have not measured this ourselves, so we will not name a winner. The fair test is the same scenario with both tools on your own hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Gatling run k6 scripts?&lt;/strong&gt;
Not according to Gatling&amp;#39;s docs, which describe no way to run k6 scripts. The two SDKs use different APIs, and Gatling&amp;#39;s &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;converters&lt;/a&gt; cover JMeter and LoadRunner, so a k6 script has to be rewritten as a Gatling simulation, even with the JavaScript SDK.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Gatling tests?&lt;/strong&gt;
No. LoadFocus runs k6 scripts and JMeter test plans. To spread a Gatling simulation across several machines, use Gatling Enterprise.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling load testing: a review of the JVM tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-jmeter/&quot;&gt;Gatling vs JMeter: tests as code or a GUI test plan?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-locust/&quot;&gt;Gatling vs Locust: JVM simulations or Python locustfiles?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/grpc-load-testing/&quot;&gt;gRPC load testing: tools, setup and what to watch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>HTTP benchmarking tools: wrk, hey, oha, ab and more</title><link>https://loadtest.app/http-benchmarking-tools/</link><guid isPermaLink="true">https://loadtest.app/http-benchmarking-tools/</guid><description>wrk, wrk2, hey, oha, ab, bombardier, autocannon and h2load compared: rate control, HTTP/2 support, latency output, licence and recent releases.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;The command-line HTTP benchmarking tools most teams reach for are wrk, wrk2, hey, oha, ab (Apache Bench), bombardier, autocannon and h2load. Use wrk2 when you need a constant request rate and honest tail latency, hey or oha for a quick check that also speaks HTTP/2, h2load when HTTP/2 or HTTP/3 is the thing under test, and autocannon when your team works in Node.js.&lt;/p&gt;
&lt;p&gt;We have not run these tools against each other, so there are no speed numbers or &amp;quot;fastest&amp;quot; verdicts here. Facts come from each project&amp;#39;s own repository, manual or releases page (checked on 9 October 2026); the &amp;quot;choose&amp;quot; notes are our judgement.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Written in&lt;/th&gt;
&lt;th&gt;Licence&lt;/th&gt;
&lt;th&gt;Rate control&lt;/th&gt;
&lt;th&gt;HTTP/2&lt;/th&gt;
&lt;th&gt;Latest release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;wrk&lt;/td&gt;
&lt;td&gt;C, LuaJIT scripts&lt;/td&gt;
&lt;td&gt;Modified Apache 2.0&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Not documented&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/wg/wrk/tags&quot;&gt;Tag 4.2.0, Feb 2021&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wrk2&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Constant (&lt;code&gt;-R&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Not documented&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/giltene/wrk2/commits/master&quot;&gt;None, last commit Sep 2019&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hey&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Limit per worker (&lt;code&gt;-q&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/rakyll/hey/releases&quot;&gt;v0.1.5, Jan 2026&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;oha&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Overall limit (&lt;code&gt;-q&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes, plus HTTP/3&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/hatoo/oha/releases&quot;&gt;v1.16.0, Aug 2026&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ab&lt;/td&gt;
&lt;td&gt;C (Apache httpd)&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://httpd.apache.org/download.cgi&quot;&gt;httpd 2.4.69, Oct 2026&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bombardier&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Limit (&lt;code&gt;-r&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/codesenberg/bombardier/releases&quot;&gt;v2.0.2, Mar 2025&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autocannon&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Limit (&lt;code&gt;-R&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/mcollina/autocannon/releases&quot;&gt;v8.0.0, Oct 2024&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;h2load&lt;/td&gt;
&lt;td&gt;C++ (nghttp2)&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Per client (&lt;code&gt;--rps&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes, plus HTTP/3&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/nghttp2/nghttp2/releases&quot;&gt;nghttp2 v1.70.0, Jul 2026&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vegeta&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Constant (&lt;code&gt;-rate&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/tsenart/vegeta/releases&quot;&gt;v12.13.0, Oct 2025&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Why does it matter whether a tool fixes the rate?&lt;/h2&gt;
&lt;p&gt;A tool that holds a fixed number of connections sends less when the server slows down, so its latency figures leave out the time requests spent waiting. A fixed-rate tool keeps sending on schedule, and the slowdown shows up in the percentiles.&lt;/p&gt;
&lt;p&gt;Most tools here run a set number of connections or workers (&lt;code&gt;-c&lt;/code&gt;), each waiting for a response before sending more. The &lt;a href=&quot;https://github.com/giltene/wrk2&quot;&gt;wrk2 README&lt;/a&gt; calls the resulting blind spot Coordinated Omission and times each request from when it was supposed to be sent. &lt;a href=&quot;https://github.com/tsenart/vegeta&quot;&gt;Vegeta&lt;/a&gt; also holds a constant rate; see our &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; roundup. oha and autocannon can correct their latency for the same effect when a rate is set, while hey and bombardier document their rate options as limits only.&lt;/p&gt;
&lt;h2&gt;Is wrk still worth using?&lt;/h2&gt;
&lt;p&gt;Yes, for a quick look at how much load one machine can push at an endpoint, read with the caveat above. Its newest tag dates from February 2021.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/wg/wrk&quot;&gt;wrk&lt;/a&gt; is written in C and keeps a fixed number of connections open across several threads. Optional LuaJIT scripts can build requests, process responses and write custom reports, and &lt;code&gt;make&lt;/code&gt; builds it together with the bundled LuaJIT and OpenSSL. It prints average, standard deviation and maximum latency; &lt;code&gt;--latency&lt;/code&gt; adds the 50th, 75th, 90th and 99th percentiles.&lt;/p&gt;
&lt;p&gt;Choose wrk2 instead when you need a fixed request rate or tail latency you can trust during a slowdown.&lt;/p&gt;
&lt;h2&gt;When do you need wrk2 instead of wrk?&lt;/h2&gt;
&lt;p&gt;When the question is how latency behaves at a given request rate. wrk2 holds that rate constant and records every latency in an HdrHistogram.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/giltene/wrk2&quot;&gt;wrk2&lt;/a&gt; adds a throughput argument (&lt;code&gt;-R&lt;/code&gt;, requests per second) to wrk. &lt;code&gt;--latency&lt;/code&gt; prints the full percentile spectrum and &lt;code&gt;--u_latency&lt;/code&gt; adds uncorrected figures for comparison. Its README warns that runs under 10 to 20 seconds may not be useful because of a 10-second calibration period, and calls the project experimental. It has no releases, and its Makefile builds a binary also named &lt;code&gt;wrk&lt;/code&gt;, so keep the two apart.&lt;/p&gt;
&lt;p&gt;Choose oha or autocannon instead when you want a rate setting with latency correction from a project that is still maintained.&lt;/p&gt;
&lt;h2&gt;Are hey and oha interchangeable?&lt;/h2&gt;
&lt;p&gt;For quick checks, largely: both run a pool of workers for a request count or a duration, and both speak HTTP/2. They differ in rate limiting and in what you see during a run.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rakyll/hey&quot;&gt;hey&lt;/a&gt; is a small Go program with prebuilt binaries for Linux, macOS and Windows and a Homebrew formula. &lt;code&gt;-q&lt;/code&gt; limits queries per second for each worker, &lt;code&gt;-h2&lt;/code&gt; turns on HTTP/2, and the summary includes a response time histogram and a latency distribution, or CSV with &lt;code&gt;-o csv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hatoo/oha&quot;&gt;oha&lt;/a&gt; is written in Rust, was inspired by hey and shows a live terminal UI. Its &lt;code&gt;-q&lt;/code&gt; sets one overall rate, &lt;code&gt;--latency-correction&lt;/code&gt; corrects for coordinated omission, and &lt;code&gt;--http-version&lt;/code&gt; accepts 1.1, 2 and, in an experimental build, 3. It installs with cargo, Homebrew, winget or pacman and can write JSON or CSV.&lt;/p&gt;
&lt;p&gt;Choose hey instead when a per-worker limit fits how you think about load; choose oha for HTTP/3, JSON or latency correction.&lt;/p&gt;
&lt;h2&gt;Should you still use ab?&lt;/h2&gt;
&lt;p&gt;For a quick check on a machine where it is already installed, yes. ab ships with the Apache HTTP Server, and its own manual lists known limits.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://httpd.apache.org/docs/2.4/programs/ab.html&quot;&gt;ab&lt;/a&gt; takes a request count (&lt;code&gt;-n&lt;/code&gt;) and a concurrency level (&lt;code&gt;-c&lt;/code&gt;), with no rate or HTTP/2 option, and uses KeepAlive only with &lt;code&gt;-k&lt;/code&gt;. Its output ends with the percentage of requests served within a given time, and it can write those percentages to CSV (&lt;code&gt;-e&lt;/code&gt;) or every measurement to a gnuplot file (&lt;code&gt;-g&lt;/code&gt;). The manual&amp;#39;s Bugs section says ab does not implement HTTP/1.x fully and warns that its heavy string parsing can make you measure ab&amp;#39;s own performance.&lt;/p&gt;
&lt;p&gt;Choose hey or oha instead when you need HTTP/2, a rate limit or keep-alive by default.&lt;/p&gt;
&lt;h2&gt;What are bombardier and autocannon good for?&lt;/h2&gt;
&lt;p&gt;bombardier is a Go tool with optional HTTP/2 and a rate limit. autocannon suits Node.js teams, because it also runs as a library inside a Node script.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/codesenberg/bombardier&quot;&gt;bombardier&lt;/a&gt; uses the fasthttp client by default and switches to Go&amp;#39;s net/http client with &lt;code&gt;--http1&lt;/code&gt; or &lt;code&gt;--http2&lt;/code&gt;. &lt;code&gt;-r&lt;/code&gt; sets a rate limit, &lt;code&gt;-l&lt;/code&gt; prints the 50th, 75th, 90th and 99th percentiles, and &lt;code&gt;-o json&lt;/code&gt; makes the result machine-readable.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mcollina/autocannon&quot;&gt;autocannon&lt;/a&gt; describes itself as an HTTP/1.1 tool with pipelining and HTTPS, installed with &lt;code&gt;npm i autocannon -g&lt;/code&gt;. &lt;code&gt;-R&lt;/code&gt; caps the overall rate and &lt;code&gt;-r&lt;/code&gt; the rate per connection, with latency corrected for coordinated omission when either is set. Its table shows the 2.5th, 50th, 97.5th and 99th percentiles, &lt;code&gt;-l&lt;/code&gt; prints them all and &lt;code&gt;-j&lt;/code&gt; writes JSON.&lt;/p&gt;
&lt;p&gt;Choose autocannon instead of bombardier when your benchmarks live in a Node.js test suite, and bombardier when you need HTTP/2.&lt;/p&gt;
&lt;h2&gt;Which tool is built for HTTP/2 and HTTP/3?&lt;/h2&gt;
&lt;p&gt;h2load. It comes from the nghttp2 project, prefers HTTP/2, and can force HTTP/1.1 or, when built with support, HTTP/3.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nghttp2.org/documentation/h2load-howto.html&quot;&gt;h2load&lt;/a&gt; is compiled with nghttp2 by passing &lt;code&gt;--enable-app&lt;/code&gt; to &lt;code&gt;./configure&lt;/code&gt;. Besides clients (&lt;code&gt;-c&lt;/code&gt;), it sets concurrent streams per client (&lt;code&gt;-m&lt;/code&gt;) to load-test multiplexing, and &lt;code&gt;--rps&lt;/code&gt; sets requests per second for each client. The &lt;a href=&quot;https://nghttp2.org/documentation/h2load.1.html&quot;&gt;manual&lt;/a&gt; reports min, max, mean, median, p95 and p99 for request time, connect time and time to first byte. HTTP/3 support is marked experimental.&lt;/p&gt;
&lt;p&gt;Choose oha instead when you want HTTP/2 with fewer options and a live view.&lt;/p&gt;
&lt;h2&gt;When should you move from a benchmark to a real load test?&lt;/h2&gt;
&lt;p&gt;When the question involves user journeys, more load than one machine can generate, or traffic from where your users are.&lt;/p&gt;
&lt;p&gt;Every tool here fires simple requests from one machine, and the h2load guide notes that the client itself can become the bottleneck. Move to a load testing tool when you need logins, multi-step journeys or think time, when traffic must come from several regions, or when a test should pass or fail in CI. Our guide on &lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;how to run a load test&lt;/a&gt; covers that step by step, and &lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery&lt;/a&gt; compares two scriptable tools.&lt;/p&gt;
&lt;p&gt;LoadFocus, the publisher of this site, &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=http-benchmarking-tools&quot;&gt;runs load tests&lt;/a&gt; for websites and APIs from 25 cloud regions, with JMeter, k6 or no-code tests. If your benchmark is a request you already send with curl, its free &lt;a href=&quot;https://loadfocus.com/curl-to-k6?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=http-benchmarking-tools&quot;&gt;cURL to k6 converter&lt;/a&gt; turns that command into a k6 script in your browser.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between wrk and wrk2?&lt;/strong&gt;
wrk2 is a fork of wrk that sends requests at a constant rate set with &lt;code&gt;-R&lt;/code&gt; and times each request from when it should have been sent. wrk keeps a fixed number of connections busy and has no rate setting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which HTTP benchmarking tools support HTTP/2?&lt;/strong&gt;
hey, oha, bombardier, h2load and Vegeta, according to their documentation. wrk and wrk2 do not document it, autocannon describes itself as HTTP/1.1 and ab has no HTTP/2 option.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools: six options compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tail-latency/&quot;&gt;Tail latency: what it is and how to measure it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/redis-benchmark/&quot;&gt;redis-benchmark: how to use it and read its numbers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Load test pass/fail criteria: how to set them</title><link>https://loadtest.app/load-test-pass-fail-criteria/</link><guid isPermaLink="true">https://loadtest.app/load-test-pass-fail-criteria/</guid><description>How to set load test pass/fail criteria: start from an SLO, choose latency, error and rate limits, split hard fails from warnings and encode them per tool.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Load test pass/fail criteria are the few limits a run must meet to count as a pass: a latency percentile for the requests users wait on, an error rate and a minimum request rate, all at a stated load. Take the limits from a service-level objective (SLO), add regression limits from earlier runs inside them, and let the tool&amp;#39;s exit code fail the run only on those hard limits. Everything else is a warning that a person reads.&lt;/p&gt;
&lt;p&gt;Tool facts were checked in each project&amp;#39;s own docs on 9 October 2026, and every limit shown is an example.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Example limit&lt;/th&gt;
&lt;th&gt;Source of the number&lt;/th&gt;
&lt;th&gt;On a miss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;p95 latency, checkout&lt;/td&gt;
&lt;td&gt;Under 400 ms&lt;/td&gt;
&lt;td&gt;Latency SLO minus a margin&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency, checkout&lt;/td&gt;
&lt;td&gt;Under 1,200 ms&lt;/td&gt;
&lt;td&gt;Earlier runs of this test&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rate&lt;/td&gt;
&lt;td&gt;Under 0.5%&lt;/td&gt;
&lt;td&gt;Availability SLO&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests per second&lt;/td&gt;
&lt;td&gt;At least 95% of plan&lt;/td&gt;
&lt;td&gt;The planned load&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load generator CPU&lt;/td&gt;
&lt;td&gt;Under 80%&lt;/td&gt;
&lt;td&gt;Tool or platform&lt;/td&gt;
&lt;td&gt;Run invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What makes a load test pass or fail?&lt;/h2&gt;
&lt;p&gt;A run passes when every hard limit holds at the planned load and fails when one is missed. It is invalid when the test itself did not do its job, whatever the numbers say.&lt;/p&gt;
&lt;p&gt;A saturated load generator, a load that never reached its target or a failing script all produce numbers about the test setup. LoadFocus, for example, shows a &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/load-generator-saturation?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=load-test-pass-fail-criteria&quot;&gt;load generator saturation&lt;/a&gt; warning when an engine peaked at or above 80% CPU. k6&amp;#39;s &lt;a href=&quot;https://github.com/grafana/k6/blob/master/errext/exitcodes/codes.go&quot;&gt;exit codes&lt;/a&gt; separate the cases for CI: 99 means thresholds failed, 107 a script exception (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How do you turn an SLO into load test criteria?&lt;/h2&gt;
&lt;p&gt;Restate each SLO that load can break, usually latency and availability, as a limit on what the test measures, and make it stricter than the SLO so a pass leaves room.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sre.google/sre-book/service-level-objectives/&quot;&gt;Google&amp;#39;s SRE book&lt;/a&gt; defines an SLO as a target value or range for a service level measured by an indicator (SLI), with examples such as 99% of &lt;code&gt;Get&lt;/code&gt; calls completing in under 100 ms. It wants an SLO to state how it is measured and when it is valid, and recommends a safety margin: an internal target tighter than the one users see.&lt;/p&gt;
&lt;p&gt;For a load test, &amp;quot;when it is valid&amp;quot; means the load and request mix: &amp;quot;95% of checkout calls under 500 ms&amp;quot; might become &amp;quot;p95 of checkout under 400 ms at 300 requests per second for 15 minutes&amp;quot;, as an example. With no SLO yet, agree a provisional one with the product owner.&lt;/p&gt;
&lt;h2&gt;Which metrics should the criteria cover?&lt;/h2&gt;
&lt;p&gt;Latency for the requests users wait on, an error rate, the request rate achieved and, mostly as warnings, server resources.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Latency per request type.&lt;/strong&gt; A global percentile mixes a fast health check with a slow search. In k6, &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;tagged requests create sub-metrics&lt;/a&gt; with their own limits; Gatling&amp;#39;s &lt;code&gt;details()&lt;/code&gt; targets one request or group. A test that slowed its own load down reports flattering percentiles (&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Errors, counted broadly.&lt;/strong&gt; A fast 200 with an error page is still a failure, so assert on content. A failed JMeter &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Response_Assertion&quot;&gt;Response Assertion&lt;/a&gt; marks its sample unsuccessful; k6 checks reach the exit code only through a threshold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A request-rate floor.&lt;/strong&gt; In a closed workload model a slow system receives fewer requests, so a run can meet its latency limits at a fraction of the planned load (&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server resources.&lt;/strong&gt; CPU, memory and pool usage come from your monitoring. Treat them as warnings unless a capacity plan sets a ceiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How do you pick the numbers?&lt;/h2&gt;
&lt;p&gt;Take the ceiling from the SLO and the regression limit from earlier runs of the same test. The SLO says what users tolerate; earlier runs say what this build normally does.&lt;/p&gt;
&lt;p&gt;The SRE book warns against setting an SLO from current performance. A regression limit exists to notice change, so current performance is exactly its input. Run the unchanged build several times with the same environment and load, note how far each metric moves, and set the limit above the worst run with some headroom, still under the SLO-derived ceiling where there is one.&lt;/p&gt;
&lt;p&gt;If runs of one build vary by more than the regression you want to catch, the test is too noisy to gate on: lengthen the steady phase or isolate the environment first. Record new reference runs after any deliberate change.&lt;/p&gt;
&lt;h2&gt;Which criteria should fail the build, and which should only warn?&lt;/h2&gt;
&lt;p&gt;Fail the build on limits that come from an SLO or check correctness. Warn on tail percentiles, resource use and slow drifts, which need judgment; a gate that fails on noise soon gets switched off.&lt;/p&gt;
&lt;p&gt;Artillery&amp;#39;s &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/ensure&quot;&gt;&lt;code&gt;ensure&lt;/code&gt; plugin&lt;/a&gt; has a warning tier: a condition with &lt;code&gt;strict: false&lt;/code&gt; is optional, so a miss leaves the exit code alone (checked on 9 October 2026). In k6, a check without a threshold never fails the run. Elsewhere, keep warnings out of the assertions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;config:
  plugins:
    ensure:
      thresholds:
        - &amp;quot;http.response_time.p95&amp;quot;: 400
      conditions:
        - expression: &amp;quot;http.response_time.p99 &amp;lt; 1200&amp;quot;
          strict: false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;When should a load test stop early?&lt;/h2&gt;
&lt;p&gt;When carrying on can only do harm or prove nothing: errors far past the limit, a system already down or a generator that cannot keep up. Give the rule a grace period so start-up noise cannot trip it.&lt;/p&gt;
&lt;p&gt;Make abort rules looser than pass limits: they protect a shared environment. k6&amp;#39;s &lt;code&gt;abortOnFail&lt;/code&gt; stops the test when a threshold fails, after an optional &lt;code&gt;delayAbortEval&lt;/code&gt;. Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/extending-locust.html&quot;&gt;extension docs&lt;/a&gt; show a background greenlet that quits once the failure ratio passes a threshold. Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/concepts/scenario/&quot;&gt;&lt;code&gt;crashLoadGeneratorIf&lt;/code&gt;&lt;/a&gt; stops the generator with a failure code when a condition in a user&amp;#39;s session holds. A JMeter &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Thread_Group&quot;&gt;Thread Group&lt;/a&gt; can stop the test on a sampler error, which suits a correctness check (all checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How does each tool encode the criteria?&lt;/h2&gt;
&lt;p&gt;k6 and Artillery turn a missed hard limit into a non-zero exit code, Locust does so for any failed request or for limits you code yourself, and Gatling&amp;#39;s assertions fail the build. JMeter leaves the verdict to a step after the run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;k6&lt;/strong&gt; &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt; sit in the script&amp;#39;s options. This example fails on the p95 of requests tagged &lt;code&gt;name: checkout&lt;/code&gt; or a 0.5% error rate, and aborts after a minute if errors pass 5%:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const options = {
  thresholds: {
    &amp;quot;http_req_duration{name:checkout}&amp;quot;: [&amp;quot;p(95)&amp;lt;400&amp;quot;],
    http_req_failed: [
      &amp;quot;rate&amp;lt;0.005&amp;quot;,
      { threshold: &amp;quot;rate&amp;lt;0.05&amp;quot;, abortOnFail: true,
        delayAbortEval: &amp;quot;1m&amp;quot; },
    ],
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Gatling&lt;/strong&gt; &lt;a href=&quot;https://docs.gatling.io/concepts/assertions/&quot;&gt;assertions&lt;/a&gt; combine a scope (&lt;code&gt;global&lt;/code&gt;, &lt;code&gt;forAll&lt;/code&gt;, &lt;code&gt;details&lt;/code&gt;), a statistic such as a percentile or &lt;code&gt;requestsPerSec&lt;/code&gt;, and a condition. They are evaluated after the run, and one failure fails the simulation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locust&lt;/strong&gt; &lt;a href=&quot;https://docs.locust.io/en/stable/running-without-web-ui.html&quot;&gt;exits with code 1 when any request failed&lt;/a&gt;, and &lt;code&gt;--exit-code-on-error&lt;/code&gt; changes that code. For a ratio or a percentile, set &lt;code&gt;environment.process_exit_code&lt;/code&gt; in a &lt;code&gt;quitting&lt;/code&gt; listener, which &lt;a href=&quot;https://github.com/locustio/locust/blob/master/locust/main.py&quot;&gt;Locust&amp;#39;s source&lt;/a&gt; honours first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JMeter&lt;/strong&gt; assertions judge single samples, and its &lt;a href=&quot;https://jmeter.apache.org/usermanual/get-started.html&quot;&gt;command-line options&lt;/a&gt; include no run-level limit. CI decides: a short script over the results file, as in our &lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;GitHub Actions guide&lt;/a&gt;, or the third-party &lt;a href=&quot;https://github.com/jmeter-maven-plugin/jmeter-maven-plugin/wiki/Test-Results-File-Format&quot;&gt;jmeter-maven-plugin&lt;/a&gt;, whose &lt;code&gt;results&lt;/code&gt; goal fails the build on any failed sample unless &lt;code&gt;errorRateThresholdInPercent&lt;/code&gt; sets a rate (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LoadFocus&lt;/strong&gt; grades runs against &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/pass-fail-thresholds?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=load-test-pass-fail-criteria&quot;&gt;pass/fail thresholds&lt;/a&gt; saved on the test: maximum p95 and p99 response times, a maximum error rate and a minimum request rate, each optional. Each run gets a PASS or FAIL badge with actual and target values, and an API hands the verdict to a pipeline. LoadFocus runs k6 arrival-rate scenarios with a fixed number of users, so there a rate floor checks what those users achieved.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Should the average response time be a pass/fail criterion?&lt;/strong&gt;
Rarely. The SRE book prefers percentiles: an average hides the long tail and can stay flat while the slowest requests get much slower.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many pass/fail criteria should a load test have?&lt;/strong&gt;
A handful of hard limits, plus warnings. The SRE book advises having as few SLOs as possible and says that if quoting one can never win a conversation about priorities, it is probably not worth having. Apply the same rule to each load test limit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can a load test pass with some failed requests?&lt;/strong&gt;
Yes, if the error rate stays under its limit. Check your tool&amp;#39;s default: Locust fails the run on any failed request.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;Load testing in GitHub Actions: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/apdex-score/&quot;&gt;Apdex score: what it is and how to calculate it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/slo-vs-sla/&quot;&gt;SLO vs SLA: the difference, SLIs and error budgets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Little&apos;s law in load testing: size and check your tests</title><link>https://loadtest.app/littles-law-load-testing/</link><guid isPermaLink="true">https://loadtest.app/littles-law-load-testing/</guid><description>Little&apos;s law in load testing explained: how users, request rate and response time fit together, how to size a closed-model test, and where it breaks.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Little&amp;#39;s law in load testing says that the average number of users or requests inside a system equals the rate at which they pass through it times the average time each one spends there: L = λW. With a fixed number of virtual users, that becomes users = loops per second × (response time + think time), which shows before a run whether a plan can reach its target rate and, after it, whether the results add up. It works on averages over a steady stretch of the test, so it says nothing about percentiles.&lt;/p&gt;
&lt;p&gt;The worked examples below are arithmetic with no test run behind them. Tool facts come from each project&amp;#39;s documentation, checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Little&amp;#39;s definition&lt;/th&gt;
&lt;th&gt;Fixed virtual users (closed model)&lt;/th&gt;
&lt;th&gt;Arrival rate (open model)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;L&lt;/td&gt;
&lt;td&gt;Average number of items in the system&lt;/td&gt;
&lt;td&gt;Virtual users, waiting or pausing&lt;/td&gt;
&lt;td&gt;Iterations in progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;λ&lt;/td&gt;
&lt;td&gt;Average arrival rate&lt;/td&gt;
&lt;td&gt;Loops completed per second&lt;/td&gt;
&lt;td&gt;The rate you set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;td&gt;Average time spent inside&lt;/td&gt;
&lt;td&gt;Response time + think time per loop&lt;/td&gt;
&lt;td&gt;Iteration duration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What does Little&amp;#39;s law say?&lt;/h2&gt;
&lt;p&gt;The long-run average number of items in a queuing system equals their average arrival rate times the average time each one spends in it. Know any two of the three and you can work out the third.&lt;/p&gt;
&lt;p&gt;John D. C. Little published the proof in 1961 in Operations Research as &lt;a href=&quot;https://doi.org/10.1287/opre.9.3.383&quot;&gt;A Proof for the Queuing Formula: L = λW&lt;/a&gt;. His &lt;a href=&quot;https://doi.org/10.1287/opre.1110.0940&quot;&gt;50th-anniversary review&lt;/a&gt; of 2011 says it holds under remarkably general conditions: for one queue or a whole network, for a subclass of items or all of them, and whatever the order of service. It also maps the terms onto a server: requests in process, requests per second and average response time.&lt;/p&gt;
&lt;h2&gt;What do L, λ and W mean in a load test?&lt;/h2&gt;
&lt;p&gt;That depends on where you draw the boundary. Around the server, L is the requests in progress and W is response time; around a virtual user&amp;#39;s whole loop, L is every virtual user and W is response time plus think time.&lt;/p&gt;
&lt;p&gt;In a closed-model test each user is always somewhere in its loop, waiting for a response or pausing between steps. With the boundary around the loop, all of them count as inside, λ is the rate at which loops complete, and W is the average loop time, including the &lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;think time&lt;/a&gt; the script adds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;users = loops per second × (response time + think time)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Around the server, the same law gives the requests in flight: requests per second × average response time, the figure your connection pools and worker threads must hold. Keep all three numbers on one boundary and one stretch of time; mixing them gives plausible wrong answers.&lt;/p&gt;
&lt;h2&gt;How do you size a closed-model test with Little&amp;#39;s law?&lt;/h2&gt;
&lt;p&gt;Turn the target request rate into loops per second and multiply by the expected time per loop: the result is the number of virtual users the plan needs. Use the response time you expect at the target load, which is usually higher than what a few users see.&lt;/p&gt;
&lt;p&gt;A worked example, arithmetic only. The goal is 100 requests per second, each loop sends 4 requests expected to take 0.5 seconds each, and the script pauses 3 times for 2 seconds on average:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;loops per second = 100 requests/s ÷ 4 requests  = 25
time per loop    = 4 × 0.5 s + 3 × 2 s          = 8 s
virtual users    = 25 loops/s × 8 s             = 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Treat 200 as the minimum. If responses take 1 second at that load, a loop lasts 10 seconds and the same target needs 250 users.&lt;/p&gt;
&lt;h2&gt;Why do the same users send fewer requests when the system slows down?&lt;/h2&gt;
&lt;p&gt;The number of users is fixed, so when the time per loop grows, the loops per second must shrink. The load falls just as the system starts to struggle.&lt;/p&gt;
&lt;p&gt;Keep the 200 users and let each request take 2 seconds. A loop now lasts 4 × 2 + 6 = 14 seconds, and 200 ÷ 14 gives about 14.3 loops, or about 57 requests per second. The test still shows 200 users while running at little over half the planned rate. The &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;open vs closed workload model&lt;/a&gt; article explains why that hides saturation; Little&amp;#39;s law predicts the size of the drop.&lt;/p&gt;
&lt;p&gt;It also shows what a capacity limit looks like. Once the request rate cannot rise, each extra user adds only waiting: at a ceiling of 25 loops per second, 300 users mean 12 seconds per loop and 400 users mean 16. LoadFocus&amp;#39;s &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/response-time-vs-virtual-users?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=littles-law-load-testing&quot;&gt;response time vs virtual users chart&lt;/a&gt; plots average and maximum response time against active virtual users, one point per user level reached, which is where that steady climb shows up.&lt;/p&gt;
&lt;h2&gt;How do you check a finished test against Little&amp;#39;s law?&lt;/h2&gt;
&lt;p&gt;Take the average request rate and response time from a stretch where the user count stayed flat, rebuild the number of users from them, and compare it with what you configured. A large gap means time is going somewhere your numbers do not show.&lt;/p&gt;
&lt;p&gt;Another arithmetic example. A run holds 200 users and reports 80 requests per second at a 0.6-second average, with the same 4 requests and 6 seconds of pauses per loop. That is 20 loops per second at 4 × 0.6 + 6 = 8.4 seconds each, enough for 168 users. The other 32 mean each loop really took 10 seconds, so about 1.6 seconds per loop is unexplained. Places to look:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connection setup. In k6, &lt;code&gt;http_req_duration&lt;/code&gt; leaves out DNS lookup and connecting (&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/metrics/reference/&quot;&gt;metrics reference&lt;/a&gt;, checked on 9 October 2026).&lt;/li&gt;
&lt;li&gt;The load generator. A busy generator delays the script between requests.&lt;/li&gt;
&lt;li&gt;Think time. Random pauses can average more than the plan says.&lt;/li&gt;
&lt;li&gt;The window. If it overlaps a ramp, fewer users were active on average.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;k6 also measures the loop directly: &lt;code&gt;vus&lt;/code&gt; counts active virtual users and &lt;code&gt;iteration_duration&lt;/code&gt; times one full iteration, so active users should land close to iterations per second × average iteration duration.&lt;/p&gt;
&lt;h2&gt;What about arrival-rate tests?&lt;/h2&gt;
&lt;p&gt;The law holds with the boundary around each iteration: iterations in progress equal the arrival rate times the iteration duration. That is how many virtual users the tool must keep ready.&lt;/p&gt;
&lt;p&gt;The k6 guide to &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/arrival-rate-vu-allocation/&quot;&gt;allocating VUs for arrival-rate executors&lt;/a&gt; estimates them as median iteration duration × rate, plus a constant for variance (checked on 9 October 2026). That is Little&amp;#39;s law with the median standing in for the mean; a few slow iterations pull the mean above the median, so the margin matters.&lt;/p&gt;
&lt;p&gt;LoadFocus runs &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=littles-law-load-testing&quot;&gt;k6 scripts&lt;/a&gt; in its cloud, and there arrival-rate scenarios run with a fixed number of users; the script validator warns about it when you add such a script. Size those tests with the closed-model arithmetic above and expect the rate to fall if responses slow down.&lt;/p&gt;
&lt;h2&gt;Where does Little&amp;#39;s law stop working?&lt;/h2&gt;
&lt;p&gt;It ties together averages over a stretch where the system is in balance. Percentiles, short windows and ramps fall outside it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Averages only. A run can satisfy the law while one request in twenty waits ten times the mean, so judge results on percentiles too.&lt;/li&gt;
&lt;li&gt;A steady stretch. The 1961 proof assumes a stationary process, and the 2011 review shows the formula is also exact over a finite window that starts and ends empty. A test plateau is neither, so pick a long one, where users finishing loops keep pace with users starting them.&lt;/li&gt;
&lt;li&gt;Ramps. While users are being added, the configured peak is not L: use the window&amp;#39;s time-averaged user count, or leave the ramp out.&lt;/li&gt;
&lt;li&gt;Errors. Fast failures count as completed requests with short times, so a failing system can still balance the equation. Check the error rate first.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Does Little&amp;#39;s law need Poisson arrivals or a particular queue?&lt;/strong&gt;
No. The proofs in Little&amp;#39;s review make no assumption about the arrival pattern or service times, and the result holds whatever the order of service. It needs only averages that exist over the period you measure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Little&amp;#39;s law predict p95 response times?&lt;/strong&gt;
No. It links three averages, so it can check a mean response time, but it says nothing about the shape of the distribution or the slowest few percent.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;Think time in load testing: what it is and how to set it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/redis-benchmark/&quot;&gt;redis-benchmark: how to use it and read its numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/amdahls-law/&quot;&gt;Amdahl&amp;#39;s law: formula, examples and scaling limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/queueing-theory/&quot;&gt;Queueing theory: what it is and why latency spikes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Locust load testing: a review of the Python tool</title><link>https://loadtest.app/locust-load-testing/</link><guid isPermaLink="true">https://loadtest.app/locust-load-testing/</guid><description>Locust load testing reviewed: Python locustfiles, web UI and headless runs, distributed mode, CI, limits, other Python tools and when k6 fits better.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Locust is a free, MIT-licensed Python load testing tool in which every test is a Python program, run by lightweight users from a web UI or the command line. Locust load testing suits teams that write Python and mostly test HTTP services. Teams that need many protocols out of the box or a GUI test builder are better served by JMeter, and teams that write JavaScript by k6.&lt;/p&gt;
&lt;p&gt;We have not run Locust for this review. The facts come from Locust&amp;#39;s own documentation, repository and PyPI page, checked on 9 October 2026, and the judgements are ours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Locust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Python 3.11 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current release&lt;/td&gt;
&lt;td&gt;2.46.7 (4 October 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written as&lt;/td&gt;
&lt;td&gt;Python classes in a locustfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ways to run&lt;/td&gt;
&lt;td&gt;Web UI on port 8089, or headless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in protocols&lt;/td&gt;
&lt;td&gt;HTTP and HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;One master and any number of workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results&lt;/td&gt;
&lt;td&gt;Live charts, CSV, HTML, JSON, OpenTelemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;Exit code 1 when any request failed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Who makes Locust, and is it still maintained?&lt;/h2&gt;
&lt;p&gt;Lars Holmberg maintains Locust, and Microsoft sponsors the work. Releases are frequent: 17 so far in 2026.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/locustio/locust&quot;&gt;Locust README&lt;/a&gt; names both. &lt;a href=&quot;https://pypi.org/project/locust/&quot;&gt;PyPI&lt;/a&gt; lists 2.46.7, published on 4 October 2026, as the latest release, with Python 3.11 or later required (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;How does a locustfile work?&lt;/h2&gt;
&lt;p&gt;A locustfile is an ordinary Python module with at least one user class. Each simulated user runs its tasks one after another and pauses for its &lt;code&gt;wait_time&lt;/code&gt; in between.&lt;/p&gt;
&lt;p&gt;Per the &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;locustfile guide&lt;/a&gt;, &lt;code&gt;HttpUser&lt;/code&gt; gives each user a &lt;code&gt;client&lt;/code&gt; that wraps python-requests, keeps cookies and records every request. Methods marked &lt;code&gt;@task&lt;/code&gt; are picked at random, weighted by the number in the decorator, and &lt;code&gt;on_start&lt;/code&gt; runs once per user. This user logs in, browses four times as often as it checks out, and starts a task at most once every 10 seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from locust import HttpUser, task, constant_pacing


class Shopper(HttpUser):
    wait_time = constant_pacing(10)

    def on_start(self):
        self.client.post(&amp;quot;/login&amp;quot;, json={&amp;quot;user&amp;quot;: &amp;quot;demo&amp;quot;})

    @task(4)
    def browse(self):
        self.client.get(&amp;quot;/products?page=2&amp;quot;, name=&amp;quot;/products&amp;quot;)

    @task
    def checkout(self):
        with self.client.post(
            &amp;quot;/checkout&amp;quot;, catch_response=True
        ) as resp:
            if &amp;quot;order_id&amp;quot; not in resp.text:
                resp.failure(&amp;quot;no order id&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;catch_response&lt;/code&gt; lets you fail a request on its content, and &lt;code&gt;name&lt;/code&gt; groups changing URLs into one row. The other wait functions are &lt;code&gt;constant&lt;/code&gt;, &lt;code&gt;between&lt;/code&gt;, &lt;code&gt;constant_throughput&lt;/code&gt; and, since &lt;a href=&quot;https://github.com/locustio/locust/releases/tag/2.46.6&quot;&gt;release 2.46.6&lt;/a&gt;, &lt;code&gt;poisson&lt;/code&gt;; our guide to &lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;think time in load testing&lt;/a&gt; explains how pauses shape the load. The docs add that wait time can only limit throughput and cannot start users to reach a target rate, so a Locust test is a fixed pool of users: a &lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;closed workload model&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Should you use the web UI or run headless?&lt;/h2&gt;
&lt;p&gt;Use the web UI while you explore and headless mode for repeatable runs.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;locust&lt;/code&gt; starts a &lt;a href=&quot;https://docs.locust.io/en/stable/quickstart.html&quot;&gt;web interface&lt;/a&gt; on port 8089, where you set the host, users and ramp-up, watch live charts and change the load mid-test. &lt;a href=&quot;https://docs.locust.io/en/stable/running-without-web-ui.html&quot;&gt;Headless mode&lt;/a&gt; takes the same settings as flags:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;locust -f locustfile.py --headless -u 200 -r 10 -t 10m \
  --host https://staging.example.com \
  --csv results --html report.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-u&lt;/code&gt; sets the users, &lt;code&gt;-r&lt;/code&gt; how many start per second and &lt;code&gt;-t&lt;/code&gt; the run time, ramp-up included. The same settings also work in &lt;code&gt;locust.conf&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;How does distributed mode work?&lt;/h2&gt;
&lt;p&gt;A master process runs the web UI and coordinates, while worker processes run the users and report back. You need one worker per CPU core, because a Python process cannot fully use more than one.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.locust.io/en/stable/running-distributed.html&quot;&gt;distributed load docs&lt;/a&gt; put one process at over a thousand requests per second for a simple test plan, so bigger tests scale out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# on the master machine
locust -f locustfile.py --master
# on each worker machine, one worker per core
locust -f - --worker --master-host 10.0.0.5 --processes -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-f -&lt;/code&gt; fetches the locustfile from the master. &lt;code&gt;--processes&lt;/code&gt; relies on &lt;code&gt;fork()&lt;/code&gt;, so it does not work on Windows, and every process needs a restart after the locustfile changes. The docs also cover a &lt;a href=&quot;https://docs.locust.io/en/stable/kubernetes-operator.html&quot;&gt;Kubernetes operator&lt;/a&gt; and &lt;a href=&quot;https://docs.locust.io/en/stable/hosted-load-testing.html&quot;&gt;Azure Load Testing&lt;/a&gt; (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;LoadFocus, which publishes this site, does not run Locust tests. It &lt;a href=&quot;https://loadfocus.com/load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=locust-load-testing&quot;&gt;runs JMeter test plans and k6 scripts&lt;/a&gt; from 25 cloud regions, so for Locust the routes above are the ways to scale.&lt;/p&gt;
&lt;h2&gt;Can Locust test protocols other than HTTP?&lt;/h2&gt;
&lt;p&gt;Yes, if you bring a Python client for the protocol. HTTP and HTTPS are the only protocols built in.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.locust.io/en/stable/testing-other-systems.html&quot;&gt;page on other protocols&lt;/a&gt; shows how to wrap a client so each call is recorded, with examples for XML-RPC and gRPC and experimental users for MQTT, Socket.IO and OpenAI. The separate &lt;a href=&quot;https://github.com/SvenskaSpel/locust-plugins&quot;&gt;locust-plugins&lt;/a&gt; project adds Kafka, FTP, Selenium and Playwright users (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;For HTTP there is also &lt;a href=&quot;https://docs.locust.io/en/stable/increase-performance.html&quot;&gt;&lt;code&gt;FastHttpUser&lt;/code&gt;&lt;/a&gt;, which uses geventhttpclient and much less CPU: the docs&amp;#39; best case for one process is about 16,000 requests per second, against 4,000 with &lt;code&gt;HttpUser&lt;/code&gt;. &lt;code&gt;FastHttpUser&lt;/code&gt; does not make single requests faster, and it skips TLS certificate checks by default.&lt;/p&gt;
&lt;h2&gt;What reports does Locust give you, and how does it fit into CI?&lt;/h2&gt;
&lt;p&gt;You get live charts in the web UI and result files on request. In CI, Locust exits with code 1 if any request failed.&lt;/p&gt;
&lt;p&gt;Per the &lt;a href=&quot;https://docs.locust.io/en/stable/configuration.html&quot;&gt;configuration reference&lt;/a&gt;, &lt;code&gt;--csv&lt;/code&gt; writes statistics, history and failures files, &lt;code&gt;--html&lt;/code&gt; saves a report and &lt;code&gt;--json&lt;/code&gt; prints the final statistics. &lt;code&gt;--otel&lt;/code&gt; exports to an &lt;a href=&quot;https://docs.locust.io/en/stable/telemetry.html&quot;&gt;OpenTelemetry&lt;/a&gt; backend. The headless docs show a listener that fails the run on failure ratio, average response time or 95th percentile, and our guide to &lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;load testing in GitHub Actions&lt;/a&gt; puts it in a workflow.&lt;/p&gt;
&lt;h2&gt;What does Locust do well, and where are its limits?&lt;/h2&gt;
&lt;p&gt;It is strong on test logic and on many concurrent, mostly idle users. It is weaker on request rate per process and on protocols you wire up yourself.&lt;/p&gt;
&lt;p&gt;Plain Python means loops, branches, generated data and your own API client all work, and tests sit in version control. The &lt;a href=&quot;https://docs.locust.io/en/stable/what-is-locust.html&quot;&gt;What is Locust&lt;/a&gt; page says the low cost of each user suits highly concurrent workloads, while granting that other tools may send more requests per second on the same hardware.&lt;/p&gt;
&lt;p&gt;Limits the docs state themselves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One Python process uses one CPU core.&lt;/li&gt;
&lt;li&gt;Libraries doing I/O in C block the process unless gevent can patch them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpUser&lt;/code&gt; does not parse HTML or load page resources.&lt;/li&gt;
&lt;li&gt;Wait time cannot add users to hold a request rate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Which other Python load testing tools are there?&lt;/h2&gt;
&lt;p&gt;Locust is the actively developed Python engine. Molotov, the main alternative, has had no release on PyPI since 2022, and Taurus runs Locust through one of its executors.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tarekziade/molotov&quot;&gt;Molotov&lt;/a&gt; is a Python 3.8+ tool built on asyncio and aiohttp; its &lt;a href=&quot;https://pypi.org/project/molotov/&quot;&gt;latest release on PyPI&lt;/a&gt; is 2.6, from 21 October 2022, and the last commit on its main branch dates from November 2023 (checked on 21 October 2026). &lt;a href=&quot;https://gettaurus.org/docs/Locust/&quot;&gt;Taurus&lt;/a&gt; runs a locustfile, or GET and POST requests written in YAML, through its &lt;code&gt;locust&lt;/code&gt; executor. You install Locust yourself, Taurus starts only the master in distributed mode, and its &lt;code&gt;throughput&lt;/code&gt; setting has no effect with this executor (checked on 21 October 2026).&lt;/p&gt;
&lt;h2&gt;When should you choose k6, JMeter or Gatling instead?&lt;/h2&gt;
&lt;p&gt;Choose another tool when its language, protocols or load model fit your team better.&lt;/p&gt;
&lt;p&gt;Our overview of &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; sets Locust next to five others.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;k6&lt;/strong&gt; instead when your team writes JavaScript, wants pass/fail &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt; in the script, or needs an open workload model, which k6&amp;#39;s &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot;&gt;arrival-rate executors&lt;/a&gt; provide (checked on 9 October 2026). LoadFocus &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=locust-load-testing&quot;&gt;runs k6 scripts&lt;/a&gt; from 25 cloud regions, though it runs arrival-rate scenarios with a fixed number of users, so for a held request rate run them with k6 itself.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;JMeter&lt;/strong&gt; instead when you need JDBC, JMS, LDAP or FTP out of the box, as its &lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;README&lt;/a&gt; lists, or when your testers build tests in a GUI. &lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter&lt;/a&gt; compares the two in detail.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;Gatling&lt;/strong&gt; instead when your team works on the JVM and wants open and closed &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;injection profiles&lt;/a&gt; in one API (checked on 9 October 2026). &lt;a href=&quot;https://loadtest.app/gatling-vs-jmeter/&quot;&gt;Gatling vs JMeter&lt;/a&gt; covers it further.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Locust free for commercial use?&lt;/strong&gt;
Yes. Locust is released under the MIT licence, which permits commercial use, modification and redistribution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which Python version does Locust need?&lt;/strong&gt;
Python 3.11 or later, according to its PyPI metadata. Install it with &lt;code&gt;pip install locust&lt;/code&gt;, or run it in a temporary environment with &lt;code&gt;uvx locust&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Locust tests?&lt;/strong&gt;
No. LoadFocus runs JMeter test plans and k6 scripts. To scale a Locust test, use its master and worker mode, the Kubernetes operator or Azure Load Testing.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-vs-jmeter/&quot;&gt;Locust vs JMeter: Python scripts or JMeter test plans?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-locust/&quot;&gt;k6 vs Locust: JavaScript scripts or Python users?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-locust/&quot;&gt;Gatling vs Locust: JVM simulations or Python locustfiles?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Locust vs JMeter: Python scripts or JMeter test plans?</title><link>https://loadtest.app/locust-vs-jmeter/</link><guid isPermaLink="true">https://loadtest.app/locust-vs-jmeter/</guid><description>Locust vs JMeter compared on licence, scripting, protocols, distributed runs, CI and reports, with plain advice on when each tool is the better choice.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Locust and Apache JMeter are both free, open source load testing tools, and the better fit depends mostly on how your team wants to build tests. Pick Locust if your team writes Python and wants test logic as ordinary code, run by lightweight event-based users. Pick JMeter if you need protocols beyond HTTP such as JDBC, JMS or LDAP, prefer to build tests in a GUI, or already own a library of &lt;code&gt;.jmx&lt;/code&gt; test plans.&lt;/p&gt;
&lt;p&gt;The facts in this Locust vs JMeter comparison come from each project&amp;#39;s own documentation and repository, checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Locust&lt;/th&gt;
&lt;th&gt;Apache JMeter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;MIT (&lt;a href=&quot;https://github.com/locustio/locust&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Apache 2.0 (&lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Python 3.11 or later&lt;/td&gt;
&lt;td&gt;Java 8 or later (release 5.6.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests written as&lt;/td&gt;
&lt;td&gt;Python code in a locustfile&lt;/td&gt;
&lt;td&gt;Test plans built in a GUI, saved as XML &lt;code&gt;.jmx&lt;/code&gt; files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;Web UI to start, adjust and watch a running test&lt;/td&gt;
&lt;td&gt;Desktop IDE to record, build and debug test plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in protocols&lt;/td&gt;
&lt;td&gt;HTTP and HTTPS; others through a Python client&lt;/td&gt;
&lt;td&gt;HTTP(S), SOAP/REST, FTP, JDBC, LDAP, JMS, mail, TCP, shell commands, Java objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual users&lt;/td&gt;
&lt;td&gt;One greenlet per user on gevent (event-based)&lt;/td&gt;
&lt;td&gt;One Java thread per user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed mode&lt;/td&gt;
&lt;td&gt;One master and any number of workers&lt;/td&gt;
&lt;td&gt;One client driving remote JMeter servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI and headless runs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--headless&lt;/code&gt;, exit code 1 when requests fail&lt;/td&gt;
&lt;td&gt;CLI mode (&lt;code&gt;jmeter -n&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reporting&lt;/td&gt;
&lt;td&gt;Live web UI charts, CSV files, HTML report&lt;/td&gt;
&lt;td&gt;HTML dashboard, live results through a Backend Listener&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between Locust and JMeter?&lt;/h2&gt;
&lt;p&gt;In Locust a test is a Python program. In JMeter a test is a plan you assemble in a desktop GUI and save as a file, and most other differences follow from that.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s &lt;a href=&quot;https://github.com/locustio/locust&quot;&gt;README&lt;/a&gt; describes tests written with plain Python loops, conditions and libraries. It installs with &lt;code&gt;pip install locust&lt;/code&gt; on Python 3.11 or later, according to its &lt;a href=&quot;https://github.com/locustio/locust/blob/master/pyproject.toml&quot;&gt;package metadata&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A JMeter test is a tree of elements: a thread group for the users, samplers for the requests, and assertions and listeners for the results. JMeter 5.6.3, the current release on the &lt;a href=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot;&gt;download page&lt;/a&gt;, requires Java 8 or later. The README on JMeter&amp;#39;s &lt;a href=&quot;https://github.com/apache/jmeter/blob/master/README.md&quot;&gt;development branch&lt;/a&gt; already calls for Java 17 (checked on 10 October 2026). When a step needs real logic, a JSR223 sampler runs a Groovy script inside the plan.&lt;/p&gt;
&lt;h2&gt;How do you write a test in each tool?&lt;/h2&gt;
&lt;p&gt;In Locust you write a Python class for each kind of user, with methods marked as tasks. In JMeter you add a thread group and samplers in the GUI, save the plan and run it from the command line.&lt;/p&gt;
&lt;p&gt;A Locust user that waits one to three seconds between tasks and lists users three times as often as it opens one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from locust import HttpUser, task, between


class ApiUser(HttpUser):
    wait_time = between(1, 3)

    @task(3)
    def list_users(self):
        self.client.get(&amp;quot;/api/users&amp;quot;)

    @task
    def view_user(self):
        self.client.get(&amp;quot;/api/users/42&amp;quot;, name=&amp;quot;/api/users/[id]&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it headless with 50 users, starting five per second, for five minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;locust -f locustfile.py --headless -u 50 -r 5 \
  --run-time 5m --host https://staging.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In JMeter the same test is a Thread Group with 50 threads and a ramp-up period, holding two HTTP Request samplers. The &lt;a href=&quot;https://jmeter.apache.org/usermanual/get-started.html&quot;&gt;getting started guide&lt;/a&gt; runs a saved plan in CLI mode, and &lt;code&gt;-e -o&lt;/code&gt; adds the HTML report at the end:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;jmeter -n -t api-users.jmx -l results.jtl -e -o report/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;.jmx&lt;/code&gt; file is XML, as the &lt;a href=&quot;https://raw.githubusercontent.com/apache/jmeter/master/bin/examples/CSVSample.jmx&quot;&gt;examples shipped with JMeter&lt;/a&gt; show, and harder to review in a pull request than Python. JMeter can also record a plan from a browser; Locust&amp;#39;s closest match is &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;har2locust&lt;/a&gt;, a beta HAR-to-locustfile converter (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;Which protocols can each tool test?&lt;/h2&gt;
&lt;p&gt;JMeter covers many more protocols out of the box. Locust ships with HTTP and HTTPS and expects you to bring a Python client for anything else.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;README&lt;/a&gt; lists HTTP and HTTPS, SOAP and REST, FTP, databases through JDBC, LDAP, JMS messaging, SMTP, POP3 and IMAP mail, TCP, native commands and Java objects. JDBC and JMS need driver jars from your database or JMS provider.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/testing-other-systems.html&quot;&gt;page on other protocols&lt;/a&gt; shows how to wrap a protocol library, with examples for gRPC, XML-RPC and MQTT, and points to locust-plugins for Kafka, Selenium and Playwright (checked on 9 October 2026). The library must cooperate with gevent, because I/O in compiled C code blocks the whole process; the docs name the PostgreSQL driver psycopg2 as one that needs an extra package. For database load tests, JMeter&amp;#39;s JDBC sampler is the shorter path.&lt;/p&gt;
&lt;h2&gt;How do virtual users work in Locust and JMeter?&lt;/h2&gt;
&lt;p&gt;JMeter runs each virtual user as a Java thread. Locust runs each user as a greenlet on gevent&amp;#39;s event loop, which keeps the cost of each user low.&lt;/p&gt;
&lt;p&gt;Each thread in a JMeter &lt;a href=&quot;https://jmeter.apache.org/usermanual/test_plan.html&quot;&gt;thread group&lt;/a&gt; runs the whole plan independently. JMeter starts with a 1 GB heap by default, which its guide says may not be enough for larger plans and thread counts.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s README says a single process can handle many thousands of concurrent users. Its &lt;a href=&quot;https://docs.locust.io/en/stable/running-distributed.html&quot;&gt;distributed load docs&lt;/a&gt; add that a Python process cannot fully use more than one CPU core, so you run one worker per core, which &lt;code&gt;--processes&lt;/code&gt; sets up on a single machine.&lt;/p&gt;
&lt;h2&gt;How do you run a test across several machines?&lt;/h2&gt;
&lt;p&gt;Both have a distributed mode built in. Locust&amp;#39;s master directs workers that run the users, and JMeter&amp;#39;s client starts the full test plan on every remote server.&lt;/p&gt;
&lt;p&gt;In Locust, one process started with &lt;code&gt;--master&lt;/code&gt; runs the web UI and tells the &lt;code&gt;--worker&lt;/code&gt; processes when to start and stop users. The Locust docs also cover a &lt;a href=&quot;https://docs.locust.io/en/stable/kubernetes-operator.html&quot;&gt;Kubernetes operator&lt;/a&gt; and &lt;a href=&quot;https://docs.locust.io/en/stable/hosted-load-testing.html&quot;&gt;Azure Load Testing&lt;/a&gt;, a Microsoft service that runs Locust tests in the cloud (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/remote-test.html&quot;&gt;remote testing mode&lt;/a&gt; lets one client control any number of servers over RMI. Each server runs the whole plan, so 1,000 threads on six servers means 6,000 threads. All nodes should run the same JMeter and Java versions, RMI needs a keystore unless SSL is disabled, and data files must be copied to each server.&lt;/p&gt;
&lt;p&gt;There is a third route for JMeter test plans. LoadFocus, which publishes this site, &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=locust-vs-jmeter&quot;&gt;runs existing JMeter test plans&lt;/a&gt; unchanged from 25 cloud regions, with no remote servers to configure. LoadFocus does not run Locust tests, so for Locust the options above remain the ways to scale.&lt;/p&gt;
&lt;h2&gt;How do they fit into CI, and what reports do you get?&lt;/h2&gt;
&lt;p&gt;Both run headless from the command line. Locust also exits with code 1 when requests fail, and JMeter produces the richer HTML report.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/running-without-web-ui.html&quot;&gt;headless docs&lt;/a&gt; show a short event listener that sets the exit code from the failure ratio, the average response time or the 95th percentile, plus a GitHub Actions example. Its &lt;code&gt;--csv&lt;/code&gt; and &lt;code&gt;--html&lt;/code&gt; options save results and a report after the run.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s documentation is firm that load tests run in CLI mode, with the GUI kept for building and debugging, and its README points to third-party libraries for Maven, Gradle and Jenkins. The &lt;a href=&quot;https://jmeter.apache.org/usermanual/generating-dashboard.html&quot;&gt;JMeter dashboard&lt;/a&gt; adds an APDEX table, a statistics table with three configurable percentiles, error summaries and response time charts.&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Choose Locust when your team writes Python and wants load tests reviewed like the rest of its code. Choose JMeter instead when you need protocols beyond HTTP, when your testers prefer a GUI, or when you already have &lt;code&gt;.jmx&lt;/code&gt; plans.&lt;/p&gt;
&lt;p&gt;Locust suits scenarios with loops, branching and generated data, tests that reuse an internal Python client for your API, and high concurrency from users who spend most of their time waiting.&lt;/p&gt;
&lt;p&gt;JMeter is the better choice for JDBC, JMS, LDAP, FTP or mail testing, where Locust would need a client you maintain yourself, and for testers who record a flow in a browser and refine it visually. A company that already runs JMeter also keeps its plans and know-how.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Locust faster than JMeter?&lt;/strong&gt;
We have not measured this ourselves, so we will not name a winner. The fair test is the same scenario with both tools on your own hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can Locust run JMeter .jmx files?&lt;/strong&gt;
Not according to its documentation, which describes tests only as Python locustfiles. Moving a JMeter test plan to Locust means rewriting it in Python.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Locust tests?&lt;/strong&gt;
No. LoadFocus runs JMeter and k6 scripts. To scale a Locust test beyond one machine, use Locust&amp;#39;s master and worker mode, its Kubernetes operator or Azure Load Testing.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust load testing: a review of the Python tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-jmeter/&quot;&gt;Gatling vs JMeter: tests as code or a GUI test plan?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-locust/&quot;&gt;k6 vs Locust: JavaScript scripts or Python users?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-locust/&quot;&gt;Gatling vs Locust: JVM simulations or Python locustfiles?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/jmeter-vs-postman/&quot;&gt;JMeter vs Postman: which tool for load testing?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Load testing in GitHub Actions: a practical guide</title><link>https://loadtest.app/load-testing-in-github-actions/</link><guid isPermaLink="true">https://loadtest.app/load-testing-in-github-actions/</guid><description>How to set up load testing in GitHub Actions with k6, Artillery, Locust or JMeter: thresholds that fail the job, artifacts and what one runner can handle.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Load testing in GitHub Actions takes one workflow file: install the tool, run a short test against a staging URL and let the tool&amp;#39;s exit code fail the job when a threshold is missed. Upload the results as an artifact so every run leaves a record. Keep the test small, because a standard GitHub-hosted runner is one virtual machine with a few CPU cores.&lt;/p&gt;
&lt;p&gt;Tool and GitHub facts below come from each vendor&amp;#39;s own documentation, checked on 9 October 2026, and the workflows follow that documented usage.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Installed by&lt;/th&gt;
&lt;th&gt;What fails the job&lt;/th&gt;
&lt;th&gt;Results to keep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;Grafana&amp;#39;s &lt;code&gt;setup-k6-action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A failed threshold&lt;/td&gt;
&lt;td&gt;JSON output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artillery&lt;/td&gt;
&lt;td&gt;&lt;code&gt;artilleryio/action-cli&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A failed &lt;code&gt;ensure&lt;/code&gt; check&lt;/td&gt;
&lt;td&gt;&lt;code&gt;report.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install locust&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any failed request, or your own rule&lt;/td&gt;
&lt;td&gt;CSV and HTML files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMeter&lt;/td&gt;
&lt;td&gt;The release archive&lt;/td&gt;
&lt;td&gt;Nothing built in: add a check step&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.jtl&lt;/code&gt; file and HTML report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What does a load test workflow need?&lt;/h2&gt;
&lt;p&gt;A trigger, a runner, a step that installs the tool, a step that runs the test with thresholds and a step that uploads the results. Everything else is optional.&lt;/p&gt;
&lt;p&gt;Keep the test script in the repository, for example under &lt;code&gt;tests/load/&lt;/code&gt;. Read the staging URL from a repository variable through the &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/contexts&quot;&gt;&lt;code&gt;vars&lt;/code&gt; context&lt;/a&gt;, and set &lt;code&gt;timeout-minutes&lt;/code&gt; on the job: &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax&quot;&gt;the default is 360 minutes&lt;/a&gt; (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;Which events should start a load test?&lt;/h2&gt;
&lt;p&gt;Run a short test on pull requests and a longer one on a schedule or by hand.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;pull_request&lt;/code&gt; to catch a regression before the merge, &lt;code&gt;workflow_dispatch&lt;/code&gt; for manual runs and &lt;code&gt;schedule&lt;/code&gt; for cron runs (UTC unless you set a timezone). GitHub&amp;#39;s &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows&quot;&gt;event reference&lt;/a&gt; adds limits (checked on 9 October 2026): schedules run only on the default branch, at most every five minutes, and can be delayed at the start of the hour. Pull requests from forks get no secrets except &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;How do you run k6 in GitHub Actions?&lt;/h2&gt;
&lt;p&gt;Use Grafana&amp;#39;s two official actions, one to install k6 and one to run the script. The step fails whenever k6 exits with a non-zero code, which a failed threshold causes.&lt;/p&gt;
&lt;p&gt;Both &lt;a href=&quot;https://github.com/grafana/setup-k6-action&quot;&gt;setup-k6-action&lt;/a&gt; and &lt;a href=&quot;https://github.com/grafana/run-k6-action&quot;&gt;run-k6-action&lt;/a&gt; are on &lt;code&gt;v1&lt;/code&gt; (checked on 9 October 2026):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: load-test
on:
  pull_request:
  workflow_dispatch:

jobs:
  k6:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    env:
      BASE_URL: ${{ vars.STAGING_URL }}
    steps:
      - uses: actions/checkout@v7
      - uses: grafana/setup-k6-action@v1
      - uses: grafana/run-k6-action@v1
        with:
          path: tests/load/smoke.js
          flags: --out json=results.json
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: k6-results
          path: results.json
          retention-days: 14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The thresholds live in the script, which reads &lt;code&gt;BASE_URL&lt;/code&gt; from the environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;

export const options = {
  vus: 10,
  duration: &amp;quot;1m&amp;quot;,
  thresholds: {
    http_req_failed: [&amp;quot;rate&amp;lt;0.01&amp;quot;],
    http_req_duration: [&amp;quot;p(95)&amp;lt;500&amp;quot;],
  },
};

export default function () {
  http.get(`${__ENV.BASE_URL}/health`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;k6&amp;#39;s documentation notes that &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;checks do not affect the exit status&lt;/a&gt;, so the pass/fail decision has to come from thresholds.&lt;/p&gt;
&lt;h2&gt;How do you run Artillery, Locust or JMeter instead?&lt;/h2&gt;
&lt;p&gt;Swap the install and run steps, keep the rest of the workflow, and use each tool&amp;#39;s own switch for failing on a missed target.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Artillery&lt;/strong&gt; has an &lt;a href=&quot;https://www.artillery.io/docs/cicd/github-actions&quot;&gt;official GitHub Action&lt;/a&gt; whose &lt;a href=&quot;https://github.com/artilleryio/action-cli&quot;&gt;README&lt;/a&gt; says &lt;code&gt;@v2&lt;/code&gt; tracks the latest 2.x release (checked on 9 October 2026). The step is &lt;code&gt;uses: artilleryio/action-cli@v2&lt;/code&gt; with &lt;code&gt;command: run --target ${{ vars.STAGING_URL }} tests/load/api.yml --output report.json&lt;/code&gt;, and the &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/ensure&quot;&gt;ensure plugin&lt;/a&gt; exits non-zero when a target is missed. The script&amp;#39;s &lt;code&gt;config&lt;/code&gt; section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;config:
  target: &amp;quot;https://staging.example.com&amp;quot;
  phases:
    - duration: 60
      arrivalRate: 5
  plugins:
    ensure:
      thresholds:
        - &amp;quot;http.response_time.p95&amp;quot;: 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Locust&lt;/strong&gt; &lt;a href=&quot;https://docs.locust.io/en/stable/running-without-web-ui.html&quot;&gt;exits with code 1 when any request failed&lt;/a&gt;, and for latency rules you set &lt;code&gt;environment.process_exit_code&lt;/code&gt; in a &lt;code&gt;quitting&lt;/code&gt; event listener, as that page shows (checked on 9 October 2026). After &lt;code&gt;actions/setup-python@v7&lt;/code&gt; with &lt;code&gt;python-version: &amp;quot;3.13&amp;quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - run: pip install locust
      - run: &amp;gt;
          locust -f tests/load/locustfile.py --headless
          -u 20 -r 5 -t 2m -H &amp;quot;$BASE_URL&amp;quot;
          --csv results --html report.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;JMeter&lt;/strong&gt; runs without its GUI through &lt;code&gt;-n&lt;/code&gt;, and the &lt;a href=&quot;https://jmeter.apache.org/usermanual/get-started.html&quot;&gt;command-line options in its user manual&lt;/a&gt; include no pass/fail threshold (checked on 9 October 2026). A short step can read the results file, CSV with a &lt;code&gt;success&lt;/code&gt; column by default, and fail the job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: &amp;quot;21&amp;quot;
      - name: Install JMeter 5.6.3
        env:
          JM_BASE: https://archive.apache.org/dist/jmeter/binaries
        run: curl -sSL &amp;quot;$JM_BASE/apache-jmeter-5.6.3.tgz&amp;quot; | tar -xzf -
      - name: Run the test plan
        run: &amp;gt;
          apache-jmeter-5.6.3/bin/jmeter -n -t tests/load/plan.jmx
          -l results.jtl -e -o report
      - name: Fail above 1% errors
        run: |
          python3 - &amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
          import csv, sys
          rows = list(csv.DictReader(open(&amp;quot;results.jtl&amp;quot;)))
          bad = sum(r[&amp;quot;success&amp;quot;] != &amp;quot;true&amp;quot; for r in rows)
          print(f&amp;quot;{bad} of {len(rows)} samples failed&amp;quot;)
          sys.exit(1 if not rows or bad &amp;gt; 0.01 * len(rows) else 0)
          EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How do you make the job fail when the test misses a target?&lt;/h2&gt;
&lt;p&gt;Write the targets into the test as thresholds and let the exit code do the rest. GitHub marks a step as failed when its command exits with a non-zero code, and the job fails with it.&lt;/p&gt;
&lt;p&gt;Start with a strict error rate and a generous latency percentile, then tighten the latency limit once you know how much your numbers move between runs. The &lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;step-by-step load testing guide&lt;/a&gt; covers choosing those targets.&lt;/p&gt;
&lt;h2&gt;Where should the results of each run go?&lt;/h2&gt;
&lt;p&gt;Upload them as a workflow artifact, with &lt;code&gt;if: always()&lt;/code&gt; so a failed test still keeps its report. Artifacts are kept for 90 days by default, and &lt;code&gt;retention-days&lt;/code&gt; shortens that.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/actions/upload-artifact&quot;&gt;upload-artifact README&lt;/a&gt; documents the 90-day default (checked on 9 October 2026). Set &lt;code&gt;if-no-files-found: error&lt;/code&gt; so a missing report fails loudly.&lt;/p&gt;
&lt;h2&gt;How much load can a GitHub-hosted runner generate?&lt;/h2&gt;
&lt;p&gt;Enough for a smoke test or a small regression check. A standard Linux runner has 4 CPUs and 16 GB of RAM in public repositories, 2 CPUs and 8 GB in private ones, and a job stops after 6 hours.&lt;/p&gt;
&lt;p&gt;Those figures come from GitHub&amp;#39;s &lt;a href=&quot;https://docs.github.com/en/actions/reference/runners/github-hosted-runners&quot;&gt;runner reference&lt;/a&gt; and &lt;a href=&quot;https://docs.github.com/en/actions/reference/limits&quot;&gt;Actions limits&lt;/a&gt; (checked on 9 October 2026), which give no figure for network bandwidth.&lt;/p&gt;
&lt;p&gt;A job runs on one machine, so it is one load generator: if its CPU is saturated, response times include time spent waiting on the runner. Every request also leaves from that machine, so rate limiters that count per client address see one very busy client. GitHub advises against allowlisting the Azure IP ranges its Ubuntu runners use; larger runners on Team and Enterprise Cloud plans add CPU, memory and static IP addresses.&lt;/p&gt;
&lt;h2&gt;When should the load come from somewhere else?&lt;/h2&gt;
&lt;p&gt;When you need more load than one machine produces, traffic from several regions, or runs longer than the job limit.&lt;/p&gt;
&lt;p&gt;With k6, &lt;a href=&quot;https://github.com/grafana/run-k6-action&quot;&gt;&lt;code&gt;run-k6-action&lt;/code&gt;&lt;/a&gt; runs the test on Grafana Cloud k6 when you set &lt;code&gt;cloud-run-locally: false&lt;/code&gt; and pass &lt;code&gt;K6_CLOUD_TOKEN&lt;/code&gt;, &lt;code&gt;K6_CLOUD_PROJECT_ID&lt;/code&gt; and, with k6 v2, &lt;code&gt;K6_CLOUD_STACK_ID&lt;/code&gt;. A &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/environment-variables/&quot;&gt;cloud run&lt;/a&gt; ignores the runner&amp;#39;s environment by default, so add &lt;code&gt;-e BASE_URL=${{ vars.STAGING_URL }}&lt;/code&gt; to &lt;code&gt;flags&lt;/code&gt; (checked on 10 October 2026). &lt;a href=&quot;https://www.artillery.io/docs/cicd/github-actions&quot;&gt;Artillery&amp;#39;s action&lt;/a&gt; can call &lt;code&gt;run-fargate&lt;/code&gt; or &lt;code&gt;run-lambda&lt;/code&gt; to generate load from your own AWS account (both checked on 9 October 2026). The &lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery comparison&lt;/a&gt; covers both.&lt;/p&gt;
&lt;p&gt;For JMeter, LoadFocus documents a &lt;a href=&quot;https://loadfocus.com/docs/guides/jmeter-load-testing/jmeter-integration-ci-cd/github-actions?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=load-testing-in-github-actions&quot;&gt;GitHub Actions integration&lt;/a&gt; built on its npm client. You save the test in your LoadFocus account, store the API key and team ID as secrets, and call &lt;code&gt;run-test&lt;/code&gt;. The flags below follow the client&amp;#39;s own help text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - run: npm install -g @loadfocus/loadfocus-api-client
      - env:
          LF_KEY: ${{ secrets.LOADFOCUS_API_KEY }}
          LF_TEAM: ${{ secrets.LOADFOCUS_TEAM_ID }}
        run: |
          test -n &amp;quot;$LF_KEY&amp;quot;
          test -n &amp;quot;$LF_TEAM&amp;quot;
          loadfocus-api config set \
            --apikey &amp;quot;$LF_KEY&amp;quot; --teamid &amp;quot;$LF_TEAM&amp;quot;
          loadfocus-api jmeter run-test --name checkout-api \
            --thresholds &amp;quot;avgresponse&amp;lt;=200,errors==0,p95&amp;lt;=250&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client waits up to 1,800 seconds by default (&lt;code&gt;--wait-timeout&lt;/code&gt; changes it), so set the job&amp;#39;s &lt;code&gt;timeout-minutes&lt;/code&gt; above 30, such as 40. It exits with code 1 when a threshold fails and with 0 when it has no API key or team ID, so the &lt;code&gt;test -n&lt;/code&gt; lines fail the step first when a secret is empty. The load comes from LoadFocus, which &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=load-testing-in-github-actions&quot;&gt;runs JMeter scripts from 25 cloud regions&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How long should a load test in a pull request run?&lt;/strong&gt;
A few minutes: long enough to get past the ramp-up and collect a stable percentile, short enough that nobody waits on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is it allowed to load test from GitHub-hosted runners?&lt;/strong&gt;
GitHub&amp;#39;s &lt;a href=&quot;https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features&quot;&gt;terms for Actions&lt;/a&gt; forbid disrupting any service and limit hosted runners to work on the repository&amp;#39;s own project (checked on 9 October 2026). Load testing that project&amp;#39;s own staging environment, without burdening GitHub&amp;#39;s servers, is the kind of testing those terms describe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I use a self-hosted runner instead?&lt;/strong&gt;
Yes. It runs on your own hardware and network, and GitHub&amp;#39;s limits page allows a self-hosted job up to 5 days. It is still one load generator.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Open vs closed workload model: which one should you use?</title><link>https://loadtest.app/open-vs-closed-workload-model/</link><guid isPermaLink="true">https://loadtest.app/open-vs-closed-workload-model/</guid><description>Open vs closed workload model explained: arrival rate or fixed users, why closed tests hide slowdowns, and how k6, Gatling, Locust and JMeter model each.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The tool facts below come from each project&amp;#39;s own documentation, checked on 9 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Open model&lt;/th&gt;
&lt;th&gt;Closed model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;You set&lt;/td&gt;
&lt;td&gt;New iterations or users per second&lt;/td&gt;
&lt;td&gt;Concurrent virtual users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When responses slow down&lt;/td&gt;
&lt;td&gt;Work keeps arriving and queues grow&lt;/td&gt;
&lt;td&gt;Users wait, so less load is sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real systems like this&lt;/td&gt;
&lt;td&gt;Most public websites and APIs&lt;/td&gt;
&lt;td&gt;Call centres, ticket queues, worker pools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;constant-arrival-rate&lt;/code&gt;, &lt;code&gt;ramping-arrival-rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;constant-vus&lt;/code&gt;, &lt;code&gt;ramping-vus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;&lt;code&gt;injectOpen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;injectClosed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;Approximated with &lt;code&gt;constant_throughput&lt;/code&gt; or &lt;code&gt;poisson&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Users with a &lt;code&gt;wait_time&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMeter&lt;/td&gt;
&lt;td&gt;Open Model Thread Group, Arrivals plugin&lt;/td&gt;
&lt;td&gt;Thread Group&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between an open and a closed workload model?&lt;/h2&gt;
&lt;p&gt;In an open model the load generator decides when work arrives, and in a closed model the system&amp;#39;s response time does.&lt;/p&gt;
&lt;p&gt;The k6 documentation puts it plainly: in the &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot;&gt;closed model&lt;/a&gt;, a virtual user&amp;#39;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 &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/what-are-virtual-users-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=open-vs-closed-workload-model&quot;&gt;virtual users&lt;/a&gt; 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.&lt;/p&gt;
&lt;h2&gt;Why does a closed model hide latency when the system saturates?&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The k6 docs describe the effect: slower responses mean longer iterations and a lower arrival rate of new ones. Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/testing-concepts/workload-models/&quot;&gt;page on workload models&lt;/a&gt; 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).&lt;/p&gt;
&lt;p&gt;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 &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/response-time-distribution?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=open-vs-closed-workload-model&quot;&gt;p95 and p99 response times&lt;/a&gt; you read at the end are only as honest as the load model behind them.&lt;/p&gt;
&lt;p&gt;The k6 docs note that some testing literature calls this problem &lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;coordinated omission&lt;/a&gt;. A 2006 USENIX paper that Gatling&amp;#39;s docs point to, &lt;a href=&quot;https://www.usenix.org/legacy/event/nsdi06/tech/full_papers/schroeder/schroeder.pdf&quot;&gt;Open Versus Closed: A Cautionary Tale&lt;/a&gt;, found that the two models give significantly different results even with the same load and service demands.&lt;/p&gt;
&lt;h2&gt;Which model matches your real traffic?&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Gatling&amp;#39;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;How do you set each model in k6?&lt;/h2&gt;
&lt;p&gt;You choose it with the executor. &lt;code&gt;constant-vus&lt;/code&gt; and &lt;code&gt;ramping-vus&lt;/code&gt; give you a closed model, while &lt;code&gt;constant-arrival-rate&lt;/code&gt; and &lt;code&gt;ramping-arrival-rate&lt;/code&gt; give you an open one.&lt;/p&gt;
&lt;p&gt;A closed scenario sets &lt;code&gt;executor: &amp;quot;constant-vus&amp;quot;&lt;/code&gt; with a number of &lt;code&gt;vus&lt;/code&gt; and a &lt;code&gt;duration&lt;/code&gt;. An open scenario sets a rate instead, and you pre-allocate enough virtual users to sustain it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const options = {
  scenarios: {
    open_model: {
      executor: &amp;quot;constant-arrival-rate&amp;quot;,
      rate: 50,
      timeUnit: &amp;quot;1s&amp;quot;,
      duration: &amp;quot;5m&amp;quot;,
      preAllocatedVUs: 100,
    },
  },
  thresholds: {
    dropped_iterations: [&amp;quot;count&amp;lt;10&amp;quot;],
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;k6 &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/arrival-rate-vu-allocation/&quot;&gt;starts an iteration only while it has a free virtual user&lt;/a&gt;, and its docs advise against leaning on &lt;code&gt;maxVUs&lt;/code&gt;, because adding users mid-test can overload the load generator. When no user is free, k6 records a &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/dropped-iterations/&quot;&gt;dropped iteration&lt;/a&gt;, which late in a test often means the system is slowing down; the threshold above fails the run at ten. Leave &lt;code&gt;sleep()&lt;/code&gt; out of the end of an arrival-rate iteration, since the executor already paces the starts (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;If you run k6 scripts on LoadFocus, its &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=open-vs-closed-workload-model&quot;&gt;k6 cloud runner&lt;/a&gt; takes the load from the test&amp;#39;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.&lt;/p&gt;
&lt;h2&gt;How do Gatling, Locust and JMeter express each model?&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gatling.&lt;/strong&gt; A scenario uses either &lt;a href=&quot;https://docs.gatling.io/concepts/injection/&quot;&gt;&lt;code&gt;injectOpen&lt;/code&gt; or &lt;code&gt;injectClosed&lt;/code&gt;&lt;/a&gt;. Open steps such as &lt;code&gt;constantUsersPerSec&lt;/code&gt; and &lt;code&gt;rampUsersPerSec&lt;/code&gt; set arrival rates, and closed steps such as &lt;code&gt;constantConcurrentUsers&lt;/code&gt; and &lt;code&gt;rampConcurrentUsers&lt;/code&gt; set concurrency. &lt;a href=&quot;https://docs.gatling.io/testing-concepts/workload-models/&quot;&gt;One injection profile cannot mix the two&lt;/a&gt; (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locust.&lt;/strong&gt; You set a number of users, and each user&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;&lt;code&gt;wait_time&lt;/code&gt;&lt;/a&gt; controls the pause between tasks. &lt;code&gt;constant_throughput&lt;/code&gt; and &lt;code&gt;constant_pacing&lt;/code&gt; 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. &lt;code&gt;poisson()&lt;/code&gt; draws random pauses, which the docs call closer to real arrivals. Their example for 500 task iterations per second is &lt;code&gt;constant_throughput(0.1)&lt;/code&gt; with 5,000 users, a closed pool with plenty of headroom (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JMeter.&lt;/strong&gt; The classic &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Thread_Group&quot;&gt;Thread Group&lt;/a&gt; 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 &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#Open_Model_Thread_Group&quot;&gt;Open Model Thread Group&lt;/a&gt;, still marked experimental, with schedules such as &lt;code&gt;rate(10/sec) random_arrivals(1 min)&lt;/code&gt;. The community &lt;a href=&quot;https://jmeter-plugins.org/wiki/ArrivalsThreadGroup/&quot;&gt;Arrivals Thread Group&lt;/a&gt; plugin starts new threads when all existing ones are busy, with a concurrency limit as a safety valve (checked on 9 October 2026).&lt;/p&gt;
&lt;h2&gt;What should you watch in an open-model test?&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Set a ceiling, such as &lt;code&gt;maxVUs&lt;/code&gt; in k6 or the plugin&amp;#39;s concurrency limit in JMeter, and treat reaching it as a result: the system could not absorb the rate you asked for.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is a closed workload model ever the right choice?&lt;/strong&gt;
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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does adding think time turn a closed model into an open one?&lt;/strong&gt;
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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many virtual users does an arrival-rate test need?&lt;/strong&gt;
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.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/coordinated-omission/&quot;&gt;Coordinated omission: what it is and how to avoid it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/think-time-in-load-testing/&quot;&gt;Think time in load testing: what it is and how to set it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/throughput-vs-latency/&quot;&gt;Throughput vs latency: how they interact under load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/amdahls-law/&quot;&gt;Amdahl&amp;#39;s law: formula, examples and scaling limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/queueing-theory/&quot;&gt;Queueing theory: what it is and why latency spikes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Think time in load testing: what it is and how to set it</title><link>https://loadtest.app/think-time-in-load-testing/</link><guid isPermaLink="true">https://loadtest.app/think-time-in-load-testing/</guid><description>Think time in load testing explained: why pauses change the load your users create, think time vs pacing, and how to set it in k6, JMeter and Locust.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;Think time in load testing is the pause a virtual user takes between two actions, standing in for the seconds a real person spends reading a page or filling in a form. It decides how hard each virtual user pushes: the longer the pause, the fewer requests per second the same number of users sends. Measure it from real-user data, randomise it around that value, and write it into the script with your tool&amp;#39;s own pause command.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Think time per loop&lt;/th&gt;
&lt;th&gt;One loop takes&lt;/th&gt;
&lt;th&gt;Requests per second from 100 users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 s&lt;/td&gt;
&lt;td&gt;1.5 s&lt;/td&gt;
&lt;td&gt;about 67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 s&lt;/td&gt;
&lt;td&gt;3.5 s&lt;/td&gt;
&lt;td&gt;about 29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 s&lt;/td&gt;
&lt;td&gt;5.5 s&lt;/td&gt;
&lt;td&gt;about 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 s&lt;/td&gt;
&lt;td&gt;10.5 s&lt;/td&gt;
&lt;td&gt;about 10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is arithmetic with no test behind it: one request per loop, a 0.5-second response, and users divided by (response time + think time).&lt;/p&gt;
&lt;h2&gt;What is think time in load testing?&lt;/h2&gt;
&lt;p&gt;Think time is the deliberate wait a test script inserts between the steps of a user journey. It models the human part of a session: reading, comparing, typing.&lt;/p&gt;
&lt;p&gt;In a shop script it sits between &amp;quot;open the product page&amp;quot; and &amp;quot;add to cart&amp;quot;, outside any request. LoadFocus&amp;#39;s &lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/what-is-delay-when-running-a-load-test?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=think-time-in-load-testing&quot;&gt;guide to delays in load testing&lt;/a&gt; separates it from a ramp-up delay, which adds users gradually, and a startup delay between users starting, and notes that many tools use the bare word &amp;quot;delay&amp;quot; for the ramp-up kind. When a tool offers a &amp;quot;delay&amp;quot; field, check which one it means.&lt;/p&gt;
&lt;h2&gt;Why does think time change the load from the same number of users?&lt;/h2&gt;
&lt;p&gt;With a fixed number of virtual users, each one waits for a response, pauses, and only then sends the next request. Its loop takes response time plus think time, so every added second of think time lowers the requests each user sends per second.&lt;/p&gt;
&lt;p&gt;One hundred users with no pause send 200 requests per second against a 0.5-second response; with a 5-second pause they send about 18, so two runs &amp;quot;at 100 users&amp;quot; can be very different tests. The k6 documentation describes the same effect for its &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/&quot;&gt;closed model&lt;/a&gt;: the next iteration starts only when the previous one finishes, so slower responses also lower the rate (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;Cutting think time to get more load from fewer users has a cost: twenty users with no pauses can match the request rate of several hundred who pause, while holding far fewer sessions and connections open than production would.&lt;/p&gt;
&lt;h2&gt;What is the difference between think time and pacing?&lt;/h2&gt;
&lt;p&gt;Think time is a pause after a step, so a loop gets longer whenever responses slow down. Pacing fixes the time from the start of one loop to the start of the next, and shortens the wait when the work took longer.&lt;/p&gt;
&lt;p&gt;With 30 seconds of pacing, a user starts a loop every 30 seconds whether its requests took 2 seconds or 20, which suits a test built from business volumes such as &amp;quot;500 orders an hour&amp;quot;. The docs for Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/api.html&quot;&gt;&lt;code&gt;constant_pacing&lt;/code&gt;&lt;/a&gt; spell out the edge case: if the task runs longer than the interval, the wait drops to zero (checked on 9 October 2026). Many scripts use both, with think time between steps and pacing on the whole loop.&lt;/p&gt;
&lt;h2&gt;Should think time be fixed or random?&lt;/h2&gt;
&lt;p&gt;Random, around an average you measured. A fixed pause keeps users that started together in step, so they hit the same page at the same moment, loop after loop.&lt;/p&gt;
&lt;p&gt;Those synchronised waves can create spikes production never sees, and a random pause spreads users out. Keep the average where your data puts it: a uniform pause between 2 and 8 seconds averages 5, the same mean load as a fixed 5-second pause. Uniform is the simple default; a normal (Gaussian) shape clusters pauses around a typical value, and an exponential one gives many short waits and a few long ones.&lt;/p&gt;
&lt;h2&gt;How do you choose think time values?&lt;/h2&gt;
&lt;p&gt;Take them from what real users do on each step, using web analytics, access logs or real-user monitoring. Use a separate value per step, because reading a product page takes longer than pressing a button.&lt;/p&gt;
&lt;p&gt;Time on page in analytics, or the gap between consecutive requests of one session in access logs, is close to the think time for that step. Use the median as the average pause and the spread to set the random range, and drop idle gaps such as a tab left open over lunch.&lt;/p&gt;
&lt;p&gt;An API called by other programs has no human in the loop, so model its request rate directly. Without data, write your assumed values into the test plan so a later run can replace them; the &lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;step-by-step guide to running a load test&lt;/a&gt; shows where they fit.&lt;/p&gt;
&lt;h2&gt;How do you add think time in k6, JMeter, Gatling, Locust and Artillery?&lt;/h2&gt;
&lt;p&gt;Each tool has its own pause command, and most also have a pacing or rate option.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Fixed pause&lt;/th&gt;
&lt;th&gt;Random pause&lt;/th&gt;
&lt;th&gt;Pacing or rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sleep(3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sleep(2 + Math.random() * 6)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Arrival-rate executors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMeter&lt;/td&gt;
&lt;td&gt;Constant Timer&lt;/td&gt;
&lt;td&gt;Uniform or Gaussian Random Timer&lt;/td&gt;
&lt;td&gt;Constant Throughput Timer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pause(3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pause(2, 8)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pace(30)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;&lt;code&gt;constant(3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;between(2, 8)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;constant_pacing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artillery&lt;/td&gt;
&lt;td&gt;&lt;code&gt;think: 3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;think&lt;/code&gt; + &lt;code&gt;jitter&lt;/code&gt; (in &lt;code&gt;config.http.defaults&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Arrival rate per phase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table above and the notes below come from each project&amp;#39;s documentation, checked on 9 October 2026; all five tools appear in our review of &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;k6.&lt;/strong&gt; &lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6/sleep/&quot;&gt;&lt;code&gt;sleep(t)&lt;/code&gt;&lt;/a&gt; pauses the virtual user for &lt;code&gt;t&lt;/code&gt; seconds; the docs build random pauses with &lt;code&gt;Math.random()&lt;/code&gt; or &lt;code&gt;randomIntBetween()&lt;/code&gt; from k6-utils:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;
import { sleep } from &amp;quot;k6&amp;quot;;

export default function () {
  http.get(&amp;quot;https://example.com/products&amp;quot;);
  sleep(2 + Math.random() * 6); // 2 to 8 seconds
  http.get(&amp;quot;https://example.com/cart&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;JMeter.&lt;/strong&gt; Among the &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html#timers&quot;&gt;timers&lt;/a&gt;, Constant Timer takes a delay in milliseconds, the Uniform and Gaussian Random Timers add a random part to a fixed offset, and Constant Throughput Timer varies its pauses to hold a target of samples per minute. A timer runs before each sampler in its scope, so one timer at thread group level pauses before every request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gatling.&lt;/strong&gt; &lt;a href=&quot;https://docs.gatling.io/concepts/scenario/&quot;&gt;&lt;code&gt;pause(2, 8)&lt;/code&gt;&lt;/a&gt; waits a uniform random time, and &lt;code&gt;pace(30)&lt;/code&gt; in a loop runs it at most once every 30 seconds. At &lt;a href=&quot;https://docs.gatling.io/concepts/simulation/&quot;&gt;simulation level&lt;/a&gt; you can switch all pauses to a normal or exponential distribution, or turn them off with &lt;code&gt;disablePauses()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locust.&lt;/strong&gt; &lt;a href=&quot;https://docs.locust.io/en/stable/writing-a-locustfile.html&quot;&gt;&lt;code&gt;wait_time&lt;/code&gt;&lt;/a&gt; runs after each task, so a task with two requests under &lt;code&gt;constant_throughput(2)&lt;/code&gt; sends four requests per second per user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Artillery.&lt;/strong&gt; A &lt;a href=&quot;https://www.artillery.io/docs/reference/engines/http&quot;&gt;&lt;code&gt;think&lt;/code&gt; step&lt;/a&gt; pauses for a number of seconds, and a &lt;code&gt;think.jitter&lt;/code&gt; setting under &lt;code&gt;config.http.defaults&lt;/code&gt; adds random variance to every think step. For two of these tools side by side, see &lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Does think time matter in an arrival-rate test?&lt;/h2&gt;
&lt;p&gt;Less, as far as the request rate goes. When the tool starts new iterations at a set rate, pauses decide how long each user stays and so how many are active at once.&lt;/p&gt;
&lt;p&gt;k6&amp;#39;s &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/constant-arrival-rate/&quot;&gt;arrival-rate executors&lt;/a&gt; pace iterations through their rate setting, and the docs advise against a sleep at the end of the iteration; Artillery&amp;#39;s &lt;a href=&quot;https://www.artillery.io/docs/reference/test-script&quot;&gt;load phases&lt;/a&gt; likewise create a set number of new virtual users per second (both checked on 9 October 2026). Think time between steps still shapes each session.&lt;/p&gt;
&lt;h2&gt;How do LoadFocus&amp;#39;s script tools set think time?&lt;/h2&gt;
&lt;p&gt;They write it into the k6 or JMeter script as a starting value for you to adjust.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://loadfocus.com/docs/guides/k6-load-testing/import-to-k6-from-curl-har-postman-openapi?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=think-time-in-load-testing&quot;&gt;import from cURL, HAR, Postman or OpenAPI&lt;/a&gt; turns a captured request into a k6 script that ends each iteration with &lt;code&gt;sleep(1)&lt;/code&gt;; treat that second as a placeholder for your measured values. LoadFocus&amp;#39;s AI test generator is instructed to give every step a think time, writes it as &lt;code&gt;sleep()&lt;/code&gt; for k6 or a Constant Timer for JMeter, and warns when a multi-step scenario has none.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Can you run a load test with zero think time?&lt;/strong&gt;
Yes, to find the maximum throughput of an endpoint or for a stress test, if you report the result as requests per second. It says little about how many real users the system can serve.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does think time count in response times?&lt;/strong&gt;
Not in request timings. In k6, &lt;code&gt;http_req_duration&lt;/code&gt; covers sending, waiting and receiving, while &lt;code&gt;iteration_duration&lt;/code&gt; covers the whole loop (&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/metrics/reference/&quot;&gt;metrics reference&lt;/a&gt;, checked on 9 October 2026). A JMeter Transaction Controller leaves timers out of its time unless you tick the option that includes them.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-vs-closed-workload-model/&quot;&gt;Open vs closed workload model: which one should you use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/littles-law-load-testing/&quot;&gt;Little&amp;#39;s law in load testing: size and check your tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/queueing-theory/&quot;&gt;Queueing theory: what it is and why latency spikes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>WebSocket load testing tools: six options compared</title><link>https://loadtest.app/websocket-load-testing-tools/</link><guid isPermaLink="true">https://loadtest.app/websocket-load-testing-tools/</guid><description>WebSocket load testing tools compared: k6, Artillery, Gatling, JMeter, Locust and Tsung, what each supports natively and when to pick another one.</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;The WebSocket load testing tools with native support are k6, Artillery, Gatling and Tsung, while JMeter needs the WebSocket Samplers plugin and Locust needs client code you write or its experimental Socket.IO user. Choose by the language your team writes, then by how closely you must check the messages the server pushes back.&lt;/p&gt;
&lt;p&gt;We have not run these tools against a WebSocket server for this article. The facts come from each project&amp;#39;s own documentation and repository, checked on 9 October 2026, and the &amp;quot;choose instead&amp;quot; notes are our judgement.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;WebSocket support&lt;/th&gt;
&lt;th&gt;Tests written in&lt;/th&gt;
&lt;th&gt;Checking what the server sends&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;Built in: &lt;code&gt;k6/websockets&lt;/code&gt;, legacy &lt;code&gt;k6/ws&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;Event handlers in your script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artillery&lt;/td&gt;
&lt;td&gt;Built-in &lt;code&gt;ws&lt;/code&gt; and &lt;code&gt;socketio&lt;/code&gt; engines&lt;/td&gt;
&lt;td&gt;YAML, plus JavaScript functions&lt;/td&gt;
&lt;td&gt;Socket.IO responses and acknowledgements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;Part of the HTTP SDK&lt;/td&gt;
&lt;td&gt;Java, Kotlin, Scala, JavaScript&lt;/td&gt;
&lt;td&gt;Checks that wait for an expected message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache JMeter&lt;/td&gt;
&lt;td&gt;WebSocket Samplers plugin&lt;/td&gt;
&lt;td&gt;Test plans built in the GUI&lt;/td&gt;
&lt;td&gt;Request-response and read samplers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;Your own client, or the experimental &lt;code&gt;SocketIOUser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Whatever your code reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tsung&lt;/td&gt;
&lt;td&gt;Built-in &lt;code&gt;ts_websocket&lt;/code&gt; session type&lt;/td&gt;
&lt;td&gt;XML&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ack&lt;/code&gt; waits for a server reply&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What makes WebSocket load different from HTTP load?&lt;/h2&gt;
&lt;p&gt;A WebSocket user opens one connection and keeps it for the whole session, and the server can send messages whenever it likes. A test therefore has two dials, open connections and messages per second, and each loads the server in its own way.&lt;/p&gt;
&lt;p&gt;k6&amp;#39;s &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/protocols/websockets/&quot;&gt;WebSocket guide&lt;/a&gt; describes full-duplex traffic over one TCP connection and explains that a virtual user stops looping over the main function and runs an event loop while the socket is open. Gatling&amp;#39;s &lt;a href=&quot;https://docs.gatling.io/reference/script/websocket/&quot;&gt;WebSocket reference&lt;/a&gt; adds that traffic flows both ways, so the HTTP request and response model no longer fits.&lt;/p&gt;
&lt;p&gt;Plan three shapes of test. Many idle connections show how many sockets your servers can hold. A smaller crowd of chatty clients tests message handling. A broadcast, where one message fans out to every subscriber, tests the push path.&lt;/p&gt;
&lt;h2&gt;What should you measure in a WebSocket load test?&lt;/h2&gt;
&lt;p&gt;Measure connect time, message latency and dropped connections, alongside how many connections stay open. These show what users of a real-time app feel.&lt;/p&gt;
&lt;p&gt;k6&amp;#39;s &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/metrics/reference/&quot;&gt;built-in metrics&lt;/a&gt; include &lt;code&gt;ws_connecting&lt;/code&gt;, &lt;code&gt;ws_ping&lt;/code&gt;, &lt;code&gt;ws_session_duration&lt;/code&gt;, &lt;code&gt;ws_sessions&lt;/code&gt; and message counts. None of them times an application message, so the delay between a client&amp;#39;s message and the server&amp;#39;s reply needs a custom Trend metric. Artillery&amp;#39;s &lt;code&gt;ws&lt;/code&gt; engine reports message counts and rates, and its Socket.IO engine adds &lt;code&gt;socketio.response_time&lt;/code&gt;, from emitting a message to receiving the response.&lt;/p&gt;
&lt;p&gt;For dropped connections, count close and error events your script did not cause, and compare sessions started with sessions still open at the end of each stage.&lt;/p&gt;
&lt;h2&gt;When is k6 the right WebSocket tool?&lt;/h2&gt;
&lt;p&gt;When your team writes JavaScript and wants each virtual user to hold several sockets at once. k6 has WebSocket support built in, with no extension to install.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6-websockets/&quot;&gt;&lt;code&gt;k6/websockets&lt;/code&gt;&lt;/a&gt; module follows the browser WebSocket API and uses a global event loop, which lets one virtual user keep multiple connections open. The older &lt;a href=&quot;https://grafana.com/docs/k6/latest/javascript-api/k6-ws/&quot;&gt;&lt;code&gt;k6/ws&lt;/code&gt;&lt;/a&gt; module is still documented, but k6 recommends &lt;code&gt;k6/websockets&lt;/code&gt; for new tests, and &lt;code&gt;k6/experimental/websockets&lt;/code&gt; is deprecated.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { WebSocket } from &amp;#39;k6/websockets&amp;#39;;

export default function () {
  const ws = new WebSocket(&amp;#39;wss://example.com/ws&amp;#39;);
  ws.onopen = () =&amp;gt; ws.send(&amp;#39;hello&amp;#39;);
  ws.onmessage = (e) =&amp;gt; {
    console.log(e.data);
    ws.close();
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;Thresholds&lt;/a&gt; work on custom metrics too, so a missed latency goal gives CI a non-zero exit code. Choose Artillery instead when your server speaks Socket.IO and you want emits and acknowledgements checked without writing client code. Our &lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery&lt;/a&gt; comparison covers the wider differences.&lt;/p&gt;
&lt;h2&gt;When does Artillery fit?&lt;/h2&gt;
&lt;p&gt;When you want short YAML scenarios, or when your server uses Socket.IO. Both engines ship with Artillery itself.&lt;/p&gt;
&lt;p&gt;A scenario on the &lt;a href=&quot;https://www.artillery.io/docs/reference/engines/websocket&quot;&gt;WebSocket engine&lt;/a&gt; is built from five actions: &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;send&lt;/code&gt;, &lt;code&gt;think&lt;/code&gt;, &lt;code&gt;loop&lt;/code&gt; and &lt;code&gt;function&lt;/code&gt;. The &lt;a href=&quot;https://www.artillery.io/docs/reference/engines/socketio&quot;&gt;Socket.IO engine&lt;/a&gt; goes further: an &lt;code&gt;emit&lt;/code&gt; can declare the response it expects, validate an acknowledgement with a JSONPath match, and share a scenario with HTTP requests, which suits apps that log in over HTTP before opening a socket.&lt;/p&gt;
&lt;p&gt;Choose k6 or Gatling instead when a test must check what a plain WebSocket server pushes back, because the &lt;code&gt;ws&lt;/code&gt; engine&amp;#39;s documentation describes no step that waits for or matches a reply.&lt;/p&gt;
&lt;h2&gt;Who should use Gatling for WebSockets?&lt;/h2&gt;
&lt;p&gt;Teams on the JVM, or on Gatling&amp;#39;s JavaScript SDK, who want message checks written into the scenario. WebSocket support is part of Gatling&amp;#39;s HTTP SDK, so HTTP and WebSocket steps share one simulation.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.gatling.io/reference/script/websocket/&quot;&gt;reference&lt;/a&gt; covers &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;sendText&lt;/code&gt;, &lt;code&gt;sendBytes&lt;/code&gt; and &lt;code&gt;close&lt;/code&gt;, plus checks that block until the expected message arrives or a timeout passes. Protocol settings can reconnect a socket the server closed, reply automatically to chosen text messages such as a ping, and buffer messages no check matched. Gatling&amp;#39;s &lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;repository&lt;/a&gt; lists WebSocket in the Apache 2.0 Community Edition, with no limits.&lt;/p&gt;
&lt;p&gt;Choose k6 instead when you prefer to react to pushed messages in event handlers over blocking checks.&lt;/p&gt;
&lt;h2&gt;Can JMeter load test WebSockets?&lt;/h2&gt;
&lt;p&gt;Yes, with a plugin. JMeter itself has no WebSocket sampler, and the usual add-on is WebSocket Samplers by Peter Doornbosch.&lt;/p&gt;
&lt;p&gt;JMeter&amp;#39;s &lt;a href=&quot;https://jmeter.apache.org/usermanual/component_reference.html&quot;&gt;component reference&lt;/a&gt; does not mention WebSocket at all. The &lt;a href=&quot;https://bitbucket.org/pjtr/jmeter-websocket-samplers/src/master/&quot;&gt;plugin&amp;#39;s repository&lt;/a&gt; describes six samplers: open connection, request-response, ping-pong, single read, single write and close. They run on the thread group&amp;#39;s own thread, support &lt;code&gt;wss&lt;/code&gt; and binary frames, and can filter out frames a test does not care about. You install it through the Plugins Manager or as a jar in &lt;code&gt;lib/ext&lt;/code&gt;; version 1.3.2 was tagged in January 2026 (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;Choose another tool when you would rather not maintain a third-party plugin on every load generator.&lt;/p&gt;
&lt;h2&gt;What about Locust and Tsung?&lt;/h2&gt;
&lt;p&gt;Locust reaches raw WebSockets only through client code you add, and Tsung supports them in XML configuration but releases rarely. Both can work, with more effort.&lt;/p&gt;
&lt;p&gt;Locust&amp;#39;s &lt;a href=&quot;https://docs.locust.io/en/stable/testing-other-systems.html&quot;&gt;documentation&lt;/a&gt; says its built-in support covers only HTTP and HTTPS; other protocols are added by wrapping a client library and firing a request event after each call. The library must be one gevent can patch, or it blocks the whole process. For Socket.IO there is an experimental &lt;code&gt;SocketIOUser&lt;/code&gt;. Choose Locust when your team writes Python and is happy to own that client code.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/processone/tsung&quot;&gt;Tsung&lt;/a&gt; is an Erlang project that calls itself a multi-protocol distributed load testing tool. Its &lt;a href=&quot;https://github.com/processone/tsung/blob/develop/docs/conf-sessions.rst&quot;&gt;session documentation&lt;/a&gt; defines WebSocket &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt; and &lt;code&gt;close&lt;/code&gt; requests, with an &lt;code&gt;ack&lt;/code&gt; attribute to wait for a response. Its &lt;a href=&quot;https://github.com/processone/tsung/tags&quot;&gt;latest tagged release&lt;/a&gt;, 1.8.0, dates from March 2023.&lt;/p&gt;
&lt;h2&gt;Where does a hosted runner like LoadFocus fit?&lt;/h2&gt;
&lt;p&gt;Holding many open sockets from your own machines takes work, and a hosted runner takes over the load generation. It fits best when your tests are already k6 scripts.&lt;/p&gt;
&lt;p&gt;LoadFocus, the publisher of this site, &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=websocket-load-testing-tools&quot;&gt;runs k6 scripts&lt;/a&gt; from 25 cloud regions, and its k6 page lists WebSocket connections among the supported k6 features. It also offers a &lt;a href=&quot;https://loadfocus.com/templates/load-testing-for-websocket-ramp-up-and-ramp-down?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=websocket-load-testing-tools&quot;&gt;WebSocket ramp-up and ramp-down template&lt;/a&gt;. We have not checked whether it runs JMeter plugins such as WebSocket Samplers, so we make no claim there.&lt;/p&gt;
&lt;h2&gt;How do you choose between them?&lt;/h2&gt;
&lt;p&gt;Start with the language your team writes, then ask whether you test raw WebSockets or Socket.IO and how strictly you must check pushed messages. That usually leaves one or two candidates.&lt;/p&gt;
&lt;p&gt;Write the same short connect, send and receive test in both and compare connect time and message latency at low load. Our guide on &lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;how to run a load test&lt;/a&gt; covers the steps, and the &lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;open source load testing tools&lt;/a&gt; roundup compares these projects beyond WebSockets.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Which WebSocket load testing tool should I start with?&lt;/strong&gt;
If your team writes JavaScript, start with k6, which has WebSocket support built in. For Socket.IO apps, Artillery&amp;#39;s engine needs the least code, and JVM teams will feel at home in Gatling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can JMeter test WebSockets without a plugin?&lt;/strong&gt;
Its component reference lists no WebSocket sampler, so the practical route is the WebSocket Samplers plugin, installed through the Plugins Manager or as a jar in &lt;code&gt;lib/ext&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should I use k6/ws or k6/websockets?&lt;/strong&gt;
Use &lt;code&gt;k6/websockets&lt;/code&gt; for new tests, as k6&amp;#39;s documentation recommends. The legacy &lt;code&gt;k6/ws&lt;/code&gt; module is still documented for existing scripts.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-load-test-a-graphql-api/&quot;&gt;How to load test a GraphQL API: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools: wrk, hey, oha, ab and more&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/grpc-load-testing/&quot;&gt;gRPC load testing: tools, setup and what to watch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/artillery-load-testing/&quot;&gt;Artillery load testing: a review of the Node.js tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tsung-load-testing/&quot;&gt;Tsung load testing: a review of the Erlang tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>How to run a load test: a step-by-step guide</title><link>https://loadtest.app/how-to-run-a-load-test/</link><guid isPermaLink="true">https://loadtest.app/how-to-run-a-load-test/</guid><description>How to run a load test from start to finish: set a goal, script one user journey, smoke test it, ramp up the load and read the results that matter.</description><pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, a load testing service mentioned in this article.&lt;/p&gt;&lt;p&gt;To run a load test, decide what a good result looks like, script one realistic user journey, check that script with a single user, then increase the load in steps while you watch response times and errors. Compare what you measured with the goal you set at the start, and change one thing at a time before the next run.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Done when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1. Set the goal&lt;/td&gt;
&lt;td&gt;Pick the load to test for and the numbers that count as a pass&lt;/td&gt;
&lt;td&gt;You can write the pass criteria in one sentence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Script a journey&lt;/td&gt;
&lt;td&gt;Turn one real user path into a script&lt;/td&gt;
&lt;td&gt;The script runs without errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Smoke test&lt;/td&gt;
&lt;td&gt;Run it with one virtual user&lt;/td&gt;
&lt;td&gt;Every check passes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Ramp up&lt;/td&gt;
&lt;td&gt;Increase the load in steps and hold at the target&lt;/td&gt;
&lt;td&gt;You reach the target or a stop condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Read the results&lt;/td&gt;
&lt;td&gt;Compare percentiles and errors with the goal&lt;/td&gt;
&lt;td&gt;You know whether it passed and what to change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What should you decide before running a load test?&lt;/h2&gt;
&lt;p&gt;Decide what load you are testing for and what result counts as a pass. Without both, a load test produces numbers but no answer.&lt;/p&gt;
&lt;p&gt;Start from real traffic if you have it. Your analytics or access logs tell you the busiest hour, its requests per second and which endpoints take most of them. Set the target above that peak, because the point is to know you have headroom. Then write the pass criteria as numbers, for example: the 95th percentile response time stays under 500 ms and fewer than 1% of requests fail at 200 virtual users.&lt;/p&gt;
&lt;p&gt;Two practical decisions belong here too. Pick an environment that looks like production in size and configuration, because a staging server with a quarter of the capacity tells you little about production. And make sure you have permission to test it: load testing a system you do not own, or one shared with other teams, without telling them can look exactly like an attack.&lt;/p&gt;
&lt;h2&gt;How do you script a realistic user journey?&lt;/h2&gt;
&lt;p&gt;Script the path that matters most to the business, one step at a time, with pauses where a real person would read or type. A script that hits one URL as fast as it can measures something, but rarely the thing you care about.&lt;/p&gt;
&lt;p&gt;For a shop: open the home page, search, open a product, add it to the cart. For an API: log in, list items, fetch one item. Add a short pause between steps (think time), use varied test data so caches behave the way they do in real life, and add a check on each response so a page of errors cannot pass as a fast response.&lt;/p&gt;
&lt;p&gt;Here is the script we used for the example run in this guide, written for k6. It ramps up to 10 virtual users, holds, and ramps down, with two pass criteria built in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;
import { check, sleep } from &amp;quot;k6&amp;quot;;

export const options = {
  stages: [
    { duration: &amp;quot;10s&amp;quot;, target: 10 },
    { duration: &amp;quot;20s&amp;quot;, target: 10 },
    { duration: &amp;quot;5s&amp;quot;, target: 0 },
  ],
  thresholds: {
    http_req_duration: [&amp;quot;p(95)&amp;lt;500&amp;quot;],
    http_req_failed: [&amp;quot;rate&amp;lt;0.01&amp;quot;],
  },
};

export default function () {
  const res = http.get(&amp;quot;http://127.0.0.1:8765/&amp;quot;);
  check(res, { &amp;quot;status is 200&amp;quot;: (r) =&amp;gt; r.status === 200 });
  sleep(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A real journey has several requests and real URLs. This one stays tiny so the steps are easy to follow.&lt;/p&gt;
&lt;h2&gt;Why run a single-user smoke test first?&lt;/h2&gt;
&lt;p&gt;A smoke test with one virtual user proves the script works before you put real load on anything. It is the cheapest way to catch a wrong URL, a missing login token or a check that always fails.&lt;/p&gt;
&lt;p&gt;With k6 you can &lt;a href=&quot;https://grafana.com/docs/k6/latest/get-started/running-k6/&quot;&gt;override the load from the command line&lt;/a&gt;, so the same script runs once with a single user:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;k6 run --vus 1 --iterations 1 script.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we ran it, the single check passed, so the script was ready for load. If a check fails at this stage, fix the script before going further: errors at one user turn into noise at a hundred.&lt;/p&gt;
&lt;h2&gt;How should you ramp up the load?&lt;/h2&gt;
&lt;p&gt;Increase the load in steps and hold each step long enough to settle, instead of jumping straight to the target. A step-wise ramp shows you the level at which things start to degrade.&lt;/p&gt;
&lt;p&gt;A common shape is to ramp up to a first step, hold for a few minutes, ramp to the next step, hold again, and so on up to the target, then ramp down. Watching each hold separately tells you whether response times climb smoothly with load or stay flat and then jump. That jump, the point where every extra user makes every request slower, is the most useful thing a load test can find. Our example holds 10 users for 20 seconds: enough to show the stages, far too short for a real test.&lt;/p&gt;
&lt;h2&gt;What should you watch while the test runs?&lt;/h2&gt;
&lt;p&gt;Watch response time percentiles, the error rate and the throughput on the load generator side, and CPU, memory and database load on the system under test. The first three tell you how users would feel, and the last three tell you why.&lt;/p&gt;
&lt;p&gt;Agree on stop conditions before you start, for example: stop if the error rate passes 5% or response times pass several seconds. Pushing a system that has already fallen over teaches nothing, and on a shared environment it gets in other people&amp;#39;s way.&lt;/p&gt;
&lt;p&gt;Watch the load generator too. If it is at full CPU, the slow responses may come from your own laptop rather than the server.&lt;/p&gt;
&lt;h2&gt;How do you read the results of a load test?&lt;/h2&gt;
&lt;p&gt;Read the percentiles and the error rate against the pass criteria you wrote down at the start. The average response time is the least useful number in the report.&lt;/p&gt;
&lt;p&gt;Here is what our example run reported (35 seconds, up to 10 virtual users, a small static page served on a laptop, run on 8 October 2026):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;282 (8 per second)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed requests&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average response time&lt;/td&gt;
&lt;td&gt;0.69 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90th percentile&lt;/td&gt;
&lt;td&gt;0.66 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95th percentile&lt;/td&gt;
&lt;td&gt;0.71 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99th percentile&lt;/td&gt;
&lt;td&gt;2.68 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slowest request&lt;/td&gt;
&lt;td&gt;15.98 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thresholds&lt;/td&gt;
&lt;td&gt;Both passed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The numbers are tiny because the target was a static file on the same machine, so they are no benchmark. The shape is the lesson: the average came out higher than the 90th percentile, because a handful of slow requests pulled it up. Percentiles describe what most requests experienced, and the 99th percentile and the slowest request show the tail that a few users hit. Judge the run on the percentile in your pass criteria, and use the tail to decide whether something needs a closer look.&lt;/p&gt;
&lt;p&gt;If a run misses its goal, change one thing (a query, a cache setting, an instance size) and run the same test again, so you know what made the difference.&lt;/p&gt;
&lt;h2&gt;When should you run the test from the cloud?&lt;/h2&gt;
&lt;p&gt;Move the load generator to the cloud when one machine cannot produce the load you need, or when it matters where your users are. A laptop on office Wi-Fi measures your office connection as much as your server.&lt;/p&gt;
&lt;p&gt;The same k6 script can run on infrastructure you manage, on Grafana&amp;#39;s hosted service, or on &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=how-to-run-a-load-test&quot;&gt;LoadFocus, which runs k6 scripts from 25 cloud regions&lt;/a&gt;. Its reports break results down by location and plot response time against the number of virtual users, which is where the jump described above becomes visible (&lt;a href=&quot;https://loadfocus.com/docs/guides/load-testing/analyzing-load-test-results?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=how-to-run-a-load-test&quot;&gt;how to read LoadFocus results&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How long should a load test run?&lt;/strong&gt;
Long enough to hold the target load for several minutes after the ramp-up, so short spikes do not decide the result. Holding for at least 10 minutes is a sensible start, and much longer when you are looking for memory leaks or slow degradation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many virtual users do I need?&lt;/strong&gt;
Work it out from real traffic. Take the busiest hour&amp;#39;s requests per second, estimate how many requests one user journey makes and how long it takes, and size the virtual users so the test produces that rate with some headroom on top.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is it safe to load test production?&lt;/strong&gt;
It can be, with care: test at a quiet time, tell the people who run the system, set stop conditions and start well below the expected peak. Many teams prefer a production-sized staging environment, so a failed test cannot affect customers.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-test-pass-fail-criteria/&quot;&gt;Load test pass/fail criteria: how to set them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;Load testing in GitHub Actions: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>k6 vs Artillery: which load testing tool fits your team?</title><link>https://loadtest.app/k6-vs-artillery/</link><guid isPermaLink="true">https://loadtest.app/k6-vs-artillery/</guid><description>k6 vs Artillery compared on scripting, protocols, pass/fail checks, scaling out and licences, with plain advice on when each tool is the better pick.</description><pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;k6 and Artillery are both open source, code-first load testing tools, and either one will load test an HTTP API well. Pick k6 if you want a single binary, test logic in JavaScript and pass/fail thresholds built in. Pick Artillery if your team already lives in Node.js, prefers short YAML scenarios, or wants Playwright browser tests and distributed runs on AWS or Azure from the command line.&lt;/p&gt;
&lt;p&gt;The facts below come from each project&amp;#39;s own documentation and repository, checked on 8 October 2026.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;k6&lt;/th&gt;
&lt;th&gt;Artillery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;AGPL-3.0 (&lt;a href=&quot;https://github.com/grafana/k6&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;MPL-2.0, some Azure modules under BSL (&lt;a href=&quot;https://github.com/artilleryio/artillery&quot;&gt;repository&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;One Go binary with an embedded JavaScript engine&lt;/td&gt;
&lt;td&gt;Node.js, latest LTS release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test scripts&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;YAML, TypeScript or JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core protocols&lt;/td&gt;
&lt;td&gt;HTTP, WebSockets, gRPC&lt;/td&gt;
&lt;td&gt;HTTP, WebSocket, Socket.IO (gRPC and Kinesis as add-on engines)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser testing&lt;/td&gt;
&lt;td&gt;Built-in browser module&lt;/td&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass/fail checks&lt;/td&gt;
&lt;td&gt;Thresholds&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ensure&lt;/code&gt; plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling out&lt;/td&gt;
&lt;td&gt;k6-operator on Kubernetes, or Grafana Cloud k6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run-lambda&lt;/code&gt; or &lt;code&gt;run-fargate&lt;/code&gt; on AWS, &lt;code&gt;run-aci&lt;/code&gt; on Azure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;What is the difference between k6 and Artillery?&lt;/h2&gt;
&lt;p&gt;They differ mainly in runtime and in how you describe load. k6 is a compiled Go tool that runs JavaScript test code, while Artillery is a Node.js tool that reads YAML or JavaScript scenarios.&lt;/p&gt;
&lt;p&gt;That difference shows up on day one. Installing k6 means one package or &lt;a href=&quot;https://grafana.com/docs/k6/latest/set-up/install-k6/&quot;&gt;a standalone binary&lt;/a&gt;, with no language runtime to manage. Artillery &lt;a href=&quot;https://www.artillery.io/docs/get-started/get-artillery&quot;&gt;needs Node.js&lt;/a&gt; and installs through npm, which feels natural in a JavaScript project and is one more runtime to keep current anywhere else.&lt;/p&gt;
&lt;p&gt;On protocols the overlap covers most web work: both speak HTTP and WebSockets out of the box. k6 also ships with gRPC, and Artillery with Socket.IO. Artillery gets gRPC and Kinesis from &lt;a href=&quot;https://www.artillery.io/docs/reference/engines&quot;&gt;add-on engines&lt;/a&gt; that you install separately (checked on 9 October 2026), and k6 reaches further through its extension ecosystem.&lt;/p&gt;
&lt;h2&gt;How do you write a test in each tool?&lt;/h2&gt;
&lt;p&gt;In k6 you write a JavaScript module with options at the top and a default function that each virtual user runs. In Artillery you usually write a YAML file with a &lt;code&gt;config&lt;/code&gt; block for the load and a &lt;code&gt;scenarios&lt;/code&gt; block for what each virtual user does.&lt;/p&gt;
&lt;p&gt;A k6 test that ramps to 20 virtual users and fails if the 95th percentile response time goes above 500 ms:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import http from &amp;quot;k6/http&amp;quot;;
import { check, sleep } from &amp;quot;k6&amp;quot;;

export const options = {
  stages: [
    { duration: &amp;quot;30s&amp;quot;, target: 20 },
    { duration: &amp;quot;1m&amp;quot;, target: 20 },
    { duration: &amp;quot;15s&amp;quot;, target: 0 },
  ],
  thresholds: { http_req_duration: [&amp;quot;p(95)&amp;lt;500&amp;quot;] },
};

export default function () {
  const res = http.get(&amp;quot;https://staging.example.com/api/users&amp;quot;);
  check(res, { &amp;quot;status is 200&amp;quot;: (r) =&amp;gt; r.status === 200 });
  sleep(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same idea in &lt;a href=&quot;https://www.artillery.io/docs/reference/test-script&quot;&gt;Artillery&amp;#39;s YAML format&lt;/a&gt;, with ten new virtual users arriving every second for a minute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;config:
  target: &amp;quot;https://staging.example.com&amp;quot;
  phases:
    - duration: 60
      arrivalRate: 10
  plugins:
    ensure:
      thresholds:
        - &amp;quot;http.response_time.p95&amp;quot;: 500
scenarios:
  - flow:
      - get:
          url: &amp;quot;/api/users&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two examples model load in different ways. The k6 script sets a number of concurrent virtual users, and the Artillery file sets how many new virtual users arrive each second. k6 can also drive arrival rates through its &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/&quot;&gt;scenario executors&lt;/a&gt;, and Artillery can cap concurrency with &lt;code&gt;maxVusers&lt;/code&gt;, so choose the model that matches how real traffic reaches your system rather than the one each tool shows first.&lt;/p&gt;
&lt;h2&gt;How do k6 and Artillery decide whether a test passed?&lt;/h2&gt;
&lt;p&gt;Both turn a performance goal into an exit code, which is what a CI pipeline needs. A missed goal ends the run with a non-zero exit code in either tool.&lt;/p&gt;
&lt;p&gt;In k6 the goals are &lt;a href=&quot;https://grafana.com/docs/k6/latest/using-k6/thresholds/&quot;&gt;thresholds&lt;/a&gt;, set in the script&amp;#39;s options as in the example above. In Artillery they live in the &lt;a href=&quot;https://www.artillery.io/docs/reference/extensions/ensure&quot;&gt;&lt;code&gt;ensure&lt;/code&gt; plugin&lt;/a&gt;, which supports simple thresholds and conditions that combine several metrics.&lt;/p&gt;
&lt;p&gt;The practical difference is where the goals sit. k6 thresholds live next to the test logic in the same file. Artillery&amp;#39;s checks live in the config block, where someone who never touches the scenarios can still adjust them.&lt;/p&gt;
&lt;h2&gt;How do you run a bigger test than one machine can generate?&lt;/h2&gt;
&lt;p&gt;Both tools can spread a test across many machines, through quite different routes. k6 leans on Kubernetes or Grafana&amp;#39;s hosted service, and Artillery leans on your own AWS or Azure account.&lt;/p&gt;
&lt;p&gt;For k6, the &lt;a href=&quot;https://grafana.com/docs/k6/latest/testing-guides/running-distributed-tests/&quot;&gt;k6-operator&lt;/a&gt; splits a test across pods in a Kubernetes cluster, and Grafana Cloud k6 runs the same scripts as a hosted service. For Artillery, the &lt;code&gt;run-lambda&lt;/code&gt; and &lt;code&gt;run-fargate&lt;/code&gt; commands &lt;a href=&quot;https://www.artillery.io/docs/load-testing-at-scale/aws-lambda&quot;&gt;launch workers in your AWS account&lt;/a&gt; after a one-time IAM setup through a CloudFormation stack. On Azure, &lt;code&gt;run-aci&lt;/code&gt; does the same with &lt;a href=&quot;https://www.artillery.io/docs/load-testing-at-scale/azure-aci&quot;&gt;Azure Container Instances&lt;/a&gt;, though using it beyond evaluation needs a paid Artillery subscription (checked on 9 October 2026). If you already run Kubernetes, the operator route will feel familiar. If you would rather not operate anything, Artillery&amp;#39;s serverless route has fewer moving parts, as long as the test can live in AWS or Azure.&lt;/p&gt;
&lt;p&gt;There is a third route for k6 scripts. &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=k6-vs-artillery&quot;&gt;LoadFocus runs k6 scripts&lt;/a&gt; on its own cloud infrastructure from 25 regions, so the script you run on your laptop becomes a distributed test without a cluster or an AWS account. LoadFocus does not run Artillery scripts, so for Artillery its own AWS and Azure commands remain the way to scale.&lt;/p&gt;
&lt;h2&gt;Where do the results go?&lt;/h2&gt;
&lt;p&gt;Both tools print a summary at the end of a run and can send metrics to other systems for dashboards and alerting.&lt;/p&gt;
&lt;p&gt;k6 offers summary statistics or granular metrics exported to the service of your choice, according to its &lt;a href=&quot;https://github.com/grafana/k6&quot;&gt;repository&lt;/a&gt;. Artillery lists more than 20 integrations for monitoring, observability and CI/CD in its &lt;a href=&quot;https://github.com/artilleryio/artillery&quot;&gt;repository&lt;/a&gt;. Either tool can feed the dashboards you already have. Check that the specific output you need is on the list before you commit, because both lists change between releases.&lt;/p&gt;
&lt;h2&gt;Which one should you choose?&lt;/h2&gt;
&lt;p&gt;Choose k6 when you want one binary in CI, JavaScript test logic with real control flow, and thresholds in the same file as the test. It also suits teams that already use Grafana, since the open source tool and the hosted service share one scripting API.&lt;/p&gt;
&lt;p&gt;Choose Artillery instead when your team writes Node.js every day, when a short YAML file is easier for the rest of the team to review than a script, or when you need Playwright browser tests and Socket.IO in the same tool. Its serverless scaling is also the shorter path if your infrastructure already lives in AWS and Kubernetes is not part of it.&lt;/p&gt;
&lt;p&gt;If you are still undecided, write the same smoke test in both. Twenty lines are enough to see which one your team finds easier to read in a code review.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is k6 free to use?&lt;/strong&gt;
Yes. k6 is open source under the AGPL-3.0 licence and runs on your own machines at no cost. Grafana Cloud k6 is a separate hosted service with its own pricing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is Artillery free to use?&lt;/strong&gt;
Mostly. Most of Artillery is under the MPL-2.0 licence, while some Azure-specific modules use a source-available licence that requires a commercial licence for production use on Azure, according to the project repository (checked on 8 October 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can LoadFocus run Artillery tests?&lt;/strong&gt;
No. LoadFocus runs k6 and JMeter scripts. To scale an Artillery test beyond one machine, use Artillery&amp;#39;s own commands for AWS Lambda, Fargate or Azure Container Instances in your own cloud account.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which tool uses fewer resources on the load generator?&lt;/strong&gt;
We have not measured this ourselves, so we will not name a winner. The k6 project says even lower-end machines can simulate a lot of traffic, and the fair way to compare is to run the same scenario with both tools on your own hardware.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-vs-k6/&quot;&gt;Gatling vs k6: JVM simulations or a single Go binary?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/open-source-load-testing-tools/&quot;&gt;Open source load testing tools: six options reviewed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/load-testing-in-github-actions/&quot;&gt;Load testing in GitHub Actions: a practical guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-locust/&quot;&gt;k6 vs Locust: JavaScript scripts or Python users?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/artillery-load-testing/&quot;&gt;Artillery load testing: a review of the Node.js tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Open source load testing tools: six options reviewed</title><link>https://loadtest.app/open-source-load-testing-tools/</link><guid isPermaLink="true">https://loadtest.app/open-source-load-testing-tools/</guid><description>Six open source load testing tools reviewed: JMeter, k6, Gatling, Locust, Artillery and Vegeta, with what each is good at and when to pick another.</description><pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p data-disclosure=&quot;feed&quot;&gt;Disclosure: loadtest.app is published by LoadFocus, which sells a load testing service covered in this article.&lt;/p&gt;&lt;p&gt;Six widely used open source load testing tools today are Apache JMeter, k6, Gatling, Locust, Artillery and Vegeta. Choose by the language your team writes tests in and by how you need to scale: JMeter for the widest protocol coverage, k6 or Artillery for JavaScript, Gatling for the JVM, Locust for Python and Vegeta for quick fixed-rate HTTP checks.&lt;/p&gt;
&lt;p&gt;We have not benchmarked these tools against each other for this article. The facts come from each project&amp;#39;s own repository and documentation, checked on 8 October 2026, and the &amp;quot;good for&amp;quot; notes are our judgement.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Licence&lt;/th&gt;
&lt;th&gt;Tests written in&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;th&gt;Spreading load across machines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Apache JMeter&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Test plans built in a desktop GUI&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Built-in remote (client-server) mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k6&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;One Go binary&lt;/td&gt;
&lt;td&gt;k6-operator on Kubernetes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gatling&lt;/td&gt;
&lt;td&gt;Apache 2.0 (some components under an enterprise licence)&lt;/td&gt;
&lt;td&gt;Java, Kotlin, Scala, JavaScript, TypeScript&lt;/td&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;td&gt;Commercial Enterprise edition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locust&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artillery&lt;/td&gt;
&lt;td&gt;MPL-2.0 (some Azure modules under BSL)&lt;/td&gt;
&lt;td&gt;YAML, JavaScript, TypeScript&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;AWS Lambda, Fargate or Azure ACI from the CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vegeta&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Command line or Go code&lt;/td&gt;
&lt;td&gt;One Go binary&lt;/td&gt;
&lt;td&gt;Run several instances&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Is Apache JMeter still worth using?&lt;/h2&gt;
&lt;p&gt;Yes, especially when you need protocols beyond HTTP. JMeter covers the widest range of the six and has the largest body of existing test plans and know-how behind it.&lt;/p&gt;
&lt;p&gt;JMeter is a Java application under the Apache 2.0 licence. Its &lt;a href=&quot;https://github.com/apache/jmeter&quot;&gt;protocol list&lt;/a&gt; covers HTTP and HTTPS, SOAP and REST web services, FTP, databases through JDBC, LDAP, JMS messaging, mail and raw TCP. You build test plans in a desktop GUI, run them headless from the command line for real tests, and use the client-server remote mode to spread a test across machines.&lt;/p&gt;
&lt;p&gt;Good for mixed-protocol systems, teams with existing JMeter plans and testers who prefer to build tests visually. Choose something else when your team wants tests as plain code that is reviewed like the application, because GUI-built XML plans are hard to read in a pull request.&lt;/p&gt;
&lt;h2&gt;When is k6 the right choice?&lt;/h2&gt;
&lt;p&gt;When your team writes JavaScript and wants tests in code with pass/fail thresholds built in. It ships as a single Go binary, which keeps CI setup simple.&lt;/p&gt;
&lt;p&gt;k6 is &lt;a href=&quot;https://github.com/grafana/k6&quot;&gt;licensed under AGPL-3.0&lt;/a&gt; and speaks HTTP, WebSockets and gRPC, with a browser module for front-end checks. Thresholds turn a goal such as a 95th percentile limit into a non-zero exit code for CI. To scale out, the &lt;a href=&quot;https://grafana.com/docs/k6/latest/testing-guides/running-distributed-tests/&quot;&gt;k6-operator&lt;/a&gt; runs distributed tests on Kubernetes, and Grafana sells a hosted service.&lt;/p&gt;
&lt;p&gt;Good for developers who keep API tests next to their code, and for CI pipelines. Choose something else when your team does not want JavaScript, or when the AGPL licence conflicts with how you embed or ship the tool. We compare it head to head in &lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Who should use Gatling?&lt;/h2&gt;
&lt;p&gt;Teams on the JVM who want tests as code and a detailed HTML report out of the box. It also offers a JavaScript and TypeScript SDK on the same engine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gatling/gatling&quot;&gt;Gatling&lt;/a&gt; runs virtual users on a non-blocking engine built on Netty, so one machine can drive a lot of load. Simulations are written in Java, Kotlin, Scala, JavaScript or TypeScript and run through Maven, Gradle, sbt or npm plugins. The community edition is Apache 2.0 and covers HTTP/1.1, HTTP/2, WebSocket, Server-Sent Events and JMS, while its gRPC, MQTT and GraphQL components use Gatling&amp;#39;s enterprise component licence.&lt;/p&gt;
&lt;p&gt;Good for Java and Scala teams, and for anyone who wants rich reports without extra tooling. Choose something else when you need to spread load across machines from the open source edition alone: generating load from managed or private infrastructure is what the commercial Gatling Enterprise is for.&lt;/p&gt;
&lt;h2&gt;Is Locust a good choice for Python teams?&lt;/h2&gt;
&lt;p&gt;Yes. Locust tests are plain Python, so loops, conditions and any Python library work the way they do in ordinary code.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/locustio/locust&quot;&gt;Locust&lt;/a&gt; is MIT-licensed and event-based on gevent, which lets a single process handle many users. Tests run from the command line or from a web UI that shows throughput, response times and errors live, and Locust distributes a test across multiple machines on its own. HTTP is built in, and other protocols are added by writing Python.&lt;/p&gt;
&lt;p&gt;Good for Python teams, scenarios with complex logic and anyone who likes watching a test in a browser. Choose something else when you want declarative, config-style tests, or when a ready-made plugin for an unusual protocol matters more than writing your own.&lt;/p&gt;
&lt;h2&gt;When does Artillery make sense?&lt;/h2&gt;
&lt;p&gt;When your team lives in Node.js, likes short YAML scenarios, or wants Playwright browser tests in the same tool as its API tests.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/artilleryio/artillery&quot;&gt;Artillery&lt;/a&gt; is mostly MPL-2.0, with some Azure modules under a source-available licence. It ships with HTTP, WebSocket and Socket.IO, adds gRPC and Kinesis through &lt;a href=&quot;https://www.artillery.io/docs/reference/engines&quot;&gt;add-on engines&lt;/a&gt;, and its &lt;code&gt;run-lambda&lt;/code&gt; and &lt;code&gt;run-fargate&lt;/code&gt; commands spread a test across workers in your own AWS account. A &lt;code&gt;run-aci&lt;/code&gt; command does the same on Azure Container Instances, with a paid Artillery subscription for anything beyond evaluation (checked on 9 October 2026).&lt;/p&gt;
&lt;p&gt;Good for Node.js teams, mixed API and browser testing, and companies already on AWS. Choose something else when you would rather not run Node.js on your load generators, or when large tests have to run outside AWS and Azure.&lt;/p&gt;
&lt;h2&gt;What is Vegeta good for?&lt;/h2&gt;
&lt;p&gt;Quick, constant-rate HTTP checks from the command line. It does one job, sending requests at a fixed rate, and keeps that job simple.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tsenart/vegeta&quot;&gt;Vegeta&lt;/a&gt; is an MIT-licensed Go tool that works as a command line program or as a Go library, with a CLI designed to compose with other Unix tools. Because it holds the request rate constant rather than modelling users, it answers questions like &amp;quot;what happens at 500 requests per second&amp;quot; better than it models user journeys.&lt;/p&gt;
&lt;p&gt;Good for smoke-checking an endpoint, fixed-rate capacity checks and shell scripts. Choose something else when you need multi-step journeys, think time or detailed reports.&lt;/p&gt;
&lt;h2&gt;Where does a hosted runner like LoadFocus fit?&lt;/h2&gt;
&lt;p&gt;None of these tools needs a hosted service, but generating large or geographically spread load from your own machines takes work. A hosted runner takes over the load generation and leaves your scripts as they are.&lt;/p&gt;
&lt;p&gt;LoadFocus, the publisher of this site, is one such service. It &lt;a href=&quot;https://loadfocus.com/jmeter-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=open-source-load-testing-tools&quot;&gt;runs existing JMeter test plans&lt;/a&gt; and &lt;a href=&quot;https://loadfocus.com/k6-load-testing?utm_source=loadtest.app&amp;amp;utm_medium=referral&amp;amp;utm_campaign=open-source-load-testing-tools&quot;&gt;k6 scripts&lt;/a&gt; from 25 cloud regions. It does not run Gatling, Locust, Artillery or Vegeta tests, so for those tools the scaling options above apply. Grafana Cloud k6 and Gatling Enterprise are the vendors&amp;#39; own hosted routes for k6 and Gatling.&lt;/p&gt;
&lt;h2&gt;How do you choose between them?&lt;/h2&gt;
&lt;p&gt;Start from the language your team already writes and the protocols you must test, then check how each tool scales. That usually narrows six options to one or two.&lt;/p&gt;
&lt;p&gt;Then write the same short test in your top two choices. Reading both side by side settles most debates faster than any feature table, and our guide on &lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;how to run a load test&lt;/a&gt; walks through that first test step by step.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Which open source load testing tool is the easiest to start with?&lt;/strong&gt;
For a single HTTP endpoint, Vegeta or k6 give you a result within minutes because each is one binary. For multi-step journeys, pick the tool whose test language your team already writes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can open source load testing tools run distributed tests?&lt;/strong&gt;
Yes, through different routes: JMeter has a built-in remote mode, Locust distributes across machines natively, k6 uses the k6-operator on Kubernetes and Artillery uses AWS Lambda, Fargate or Azure Container Instances. For Gatling, load generation from managed or private infrastructure is part of the commercial Enterprise edition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is JMeter better than k6?&lt;/strong&gt;
Neither is better in general. JMeter covers more protocols and suits GUI-based test design, while k6 suits developers who want tests as JavaScript code in CI.&lt;/p&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/http-benchmarking-tools/&quot;&gt;HTTP benchmarking tools: wrk, hey, oha, ab and more&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/locust-load-testing/&quot;&gt;Locust load testing: a review of the Python tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/gatling-load-testing/&quot;&gt;Gatling load testing: a review of the JVM tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/websocket-load-testing-tools/&quot;&gt;WebSocket load testing tools: six options compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/k6-vs-artillery/&quot;&gt;k6 vs Artillery: which load testing tool fits your team?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/how-to-run-a-load-test/&quot;&gt;How to run a load test: a step-by-step guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/azure-load-testing/&quot;&gt;Azure Load Testing: a review of Microsoft&amp;#39;s service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/artillery-load-testing/&quot;&gt;Artillery load testing: a review of the Node.js tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/nbomber-load-testing/&quot;&gt;NBomber load testing: a review of the .NET framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/tsung-load-testing/&quot;&gt;Tsung load testing: a review of the Erlang tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/pgbench/&quot;&gt;pgbench: how to benchmark and load test PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loadtest.app/sysbench/&quot;&gt;How to use sysbench for CPU, disk and MySQL benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>