How to Measure Throughput and Concurrency Limits Through a Proxy in k6 and JMeter
Table of contents
- Introduction: why know the limit in advance, not during a large data pull
- Preparation: tools, requirements, and access
- Basic concepts in plain language
- Step 1: preparing the test target and proxy data
- Step 2: understanding the correct load testing methodology
- Step 3: k6 — ready-made script with proxy and load steps
- Step 4: jmeter — the same scenario with proxy settings in the test plan
- Step 5: distinguishing proxy limit from client limit and server limit — three checks
- Step 6: what to do with the results — threads, pool, and data pull time
- Step 7: load testing ethics and gentle modes
- Final verification: readiness checklist
- Typical mistakes and solutions
- Additional features and optimization
- Faq: common questions about measuring throughput through a proxy
- Conclusion
Working through a proxy almost always comes down to one question: how many parallel requests can the chain of “your client - proxy - target server” actually handle before latencies spike and errors start piling up. This guide will teach you how to measure this in advance, not at the moment of a big data pull when you're already counting hours of downtime.
Introduction: Why Know the Limit in Advance, Not During a Large Data Pull
Imagine a typical scenario. You launch a large task to collect public data through a pool of proxies. Everything goes well for the first few thousand requests, but then latency grows, some requests start failing with timeouts, and you can't tell — is your code, the proxy, or the target server to blame? By this point, you've already lost time and possibly data.
It's much smarter to run a controlled load test in advance and find the point of degradation. Then you can choose a safe number of threads and properly plan your data pull time.
What the Reader Will Get in the End
After completing this guide, you'll be able to independently:
- Build a correct load testing scenario in k6 and run it through a proxy.
- Replicate the same scenario in JMeter with proxy settings in the test plan.
- Read the report: understand RPS (requests per second), percentile latencies, and error rate.
- Differentiate between the proxy limit, your client's limit, and the target server's limit.
- Convert results into thread counts, pool size, and expected data pull time.
Who This Guide Is For
This material is aimed at mid-level engineers, data analysts, and developers. If you've already written simple JavaScript scripts or run programs from the command line, you'll feel right at home. For beginners, we explain every term in plain language.
What You Need to Know in Advance
Basic skills are enough: how to open a terminal, install a program, and edit a text file. Knowledge of HTTP at the level of “what is a request and response” is a plus, but not required.
How Much Time It Will Take
Installing the tools will take about 30-40 minutes. You'll run your first working test in k6 within an hour. The full cycle with JMeter and analysis will take 3-4 hours of steady work.
⚠️ Attention: All examples in this guide are intended for testing your own staging environment or resources for which you have explicit permission to load test. Load testing third-party services without consent is unacceptable. We'll discuss ethics separately at the end.
Preparation: Tools, Requirements, and Access
Before measuring anything, we need to set up a working environment. Here we'll list everything you need and show how to install each component.
Required Tools and Access
- k6 — a lightweight load testing tool; test scripts are written in JavaScript.
- Apache JMeter — a classic tool with a Java-based GUI.
- Java 17 or newer — required for JMeter.
- Access to a Proxeon proxy: address, port, username, and password. You get these from your service dashboard.
- Test target — your own staging environment or an approved resource.
System Requirements
For comfortable work, a machine with 4 CPU cores and 8 GB of RAM will suffice. For high loads (thousands of virtual users), it's advisable to have 8 cores and 16 GB. The operating system can be Windows, macOS, or Linux; all tools are cross-platform.
Tip: Run the load generator on a separate machine or in a separate cloud server. This way you won't confuse the load on your laptop with the load on the proxy.
Installing k6
- Use the official installation method for your system: on macOS via the Homebrew package manager with the command brew install k6.
- On Windows, use the Chocolatey package manager: choco install k6.
- On Linux, download the package from the project repository and install it via your system package manager.
- Verify the installation with k6 version — you'll see the version number.
✅ Verification: If the k6 version command outputs a version string and doesn't throw a “command not found” error, the tool is installed correctly.
Installing Java and JMeter
- Install OpenJDK 17 or newer and verify with java -version.
- Download the Apache JMeter archive from the project's official website.
- Unzip the archive into a convenient folder, e.g., your home directory.
- Navigate to the bin subfolder inside the extracted JMeter folder.
- Run the jmeter file (on Linux and macOS) or jmeter.bat (on Windows).
- Wait for the GUI window to appear with the test plan tree on the left.
✅ Verification: If the JMeter window opens with a “Test Plan” element in the tree on the left, everything is ready.
Backups and Staging Preparation
If you're testing your own service, take a snapshot or backup of the staging data beforehand. The load should not affect production. Whenever possible, use a copy of the environment rather than the production server.
Basic Concepts in Plain Language
To read reports and avoid confusion, let's break down the key terms.
Throughput and RPS
Throughput is how much useful work the system performs per unit of time. In the context of HTTP, it's usually measured in RPS — requests per second. The higher the RPS at acceptable latencies, the better.
Latency and Percentiles
Latency is the time from sending a request to receiving a response. A single average value is misleading because rare slow requests are diluted within it. Therefore, we use percentiles.
P95 percentile means: 95 percent of requests were faster than this value, while 5 percent were slower. The p99 percentile shows the behavior of the slowest requests. P95 and p99 are the ones that best reflect real user experience under load.
Error Rate and the Point of Degradation
Error rate is the percentage of requests that ended unsuccessfully: timeouts, connection refused, 5xx response codes. The point of degradation is where an increase in load stops increasing RPS, while latencies and errors spike sharply. This is the limit we're looking for.
Concurrency and Virtual Users
Concurrency is how many requests are being executed simultaneously. In k6, it's set through VU (virtual users); in JMeter, it's the number of threads in the Thread Group. One VU or thread sends requests sequentially, and many of them create parallel load.
Proxy in the Load Chain
A proxy is an intermediary node through which your requests travel. It has its own limit on simultaneous connections and throughput. Our goal is to determine at what concurrency level this node becomes the bottleneck.
Tip: Keep Little's Law in mind: the average number of simultaneous requests is roughly equal to RPS multiplied by the average latency in seconds. It helps quickly estimate needed concurrency.
Step 1: Preparing the Test Target and Proxy Data
Goal of this step: Get a working target URL and a properly formatted proxy connection string.
- Determine the target URL you'll load test. Let's use your own staging environment, e.g., https://stage.example.com/api/health.
- Verify the target responds quickly and stably to a single request via a regular browser or curl.
- Get your Proxeon proxy data from the dashboard: host, port, username, and password.
- Construct the connection string in the format http://USERNAME:PASSWORD@HOST:PORT. For example: http://user:pass@proxy.proxeon.net:8000.
- Test the proxy with a single curl request, substituting your own data.
Example test request:
curl -x http://user:pass@proxy.proxeon.net:8000 -H "Accept: application/json" https://stage.example.com/api/health⚠️ Attention: Never store proxy credentials directly in code that will be committed to a repository. Use environment variables instead. We'll show this in the next steps.
Expected result: Curl through the proxy returns a correct response from your target without connection errors.
Possible issues: If curl hangs — check the port and firewall. If you get a 407 authentication error — double-check your username and password.
✅ Verification: You see the response body from the target, obtained specifically through the proxy. This means the chain works.
Step 2: Understanding the Correct Load Testing Methodology
Goal of this step: Understand why a one-time burst is useless and how to build a correct load profile.
Why a One-Time Burst Is Misleading
If you launch a thousand requests all at once, you'll get a nice-looking but meaningless number. Such a burst doesn't show sustained behavior. The system might absorb a peak spike thanks to buffers and then degrade. Or it might choke at the start due to cold connections.
Three Phases of a Proper Test
A correct load test consists of three phases:
- Warm-up. For the first 30-60 seconds, gradually ramp up the load. Connection pools, DNS caches, and internal structures warm up. Don't include warm-up data in the final statistics.
- Step ramp-up. Increase concurrency in steps: e.g., 10, 25, 50, 100, 200 VU, holding at each step for 1-2 minutes. This way we see at which step degradation begins.
- Steady state. Fix the load at a level slightly below the limit and hold it for 5-10 minutes. The plateau shows whether the system is stable under constant load, whether there are leaks or accumulating queues.
Tip: Never make conclusions based on the first 30 seconds of the test. Let the system reach a steady state, then look at the numbers.
What to Record at Each Step
- Achieved RPS.
- p50, p95, p99 latencies.
- Error rate.
- Number of active VUs or threads.
We determine the point of degradation like this: find the step after which RPS stops growing, while p95 and error rate start increasing sharply. The previous step is your safe working point.
✅ Verification: You can explain in your own words how warm-up differs from the plateau and why step ramp-up is needed.
Step 3: k6 — Ready-Made Script with Proxy and Load Steps
Goal of this step: Create and run a working k6 script that goes through warm-up, steps, and plateau via a proxy.
How k6 Works with a Proxy
k6 reads the proxy address from the HTTP_PROXY and HTTPS_PROXY environment variables. This is convenient: you don't need to hardcode data in the script, just pass it at launch.
Ready-Made Script load-test.js
Create a file named load-test.js and paste the following code into it. Replace the target URL with your own.
import http from 'k6/http'; import { check, sleep } from 'k6'; import { Trend, Rate, Counter } from 'k6/metrics'; const latency = new Trend('req_latency', true); const errors = new Rate('req_errors'); const ok = new Counter('req_ok'); export const options = { discardResponseBodies: true, thresholds: { req_errors: ['rate<0.02'], http_req_duration: ['p(95)<1500', 'p(99)<3000'], }, scenarios: { ramp: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '1m', target: 10 }, { duration: '2m', target: 25 }, { duration: '2m', target: 50 }, { duration: '2m', target: 100 }, { duration: '2m', target: 200 }, { duration: '5m', target: 200 }, { duration: '1m', target: 0 }, ], gracefulRampDown: '30s', }, }, }; const TARGET = __ENV.TARGET_URL || 'https://stage.example.com/api/health'; export default function () { const res = http.get(TARGET, { timeout: '10s', tags: { name: 'target' } }); latency.add(res.timings.duration); const good = check(res, { 'status is 200': (r) => r.status === 200 }); errors.add(!good); if (good) ok.add(1); sleep(0.5); }How to Run the Script Through a Proxy
- Open a terminal in the script folder.
- Set environment variables with the proxy address and target. On Linux and macOS, use export.
- Run k6 with the script run command.
Example run on Linux and macOS:
export HTTP_PROXY=http://user:pass@proxy.proxeon.net:8000 export HTTPS_PROXY=http://user:pass@proxy.proxeon.net:8000 export TARGET_URL=https://stage.example.com/api/health k6 run load-test.jsOn Windows in PowerShell, set variables using the $env syntax:
$env:HTTP_PROXY="http://user:pass@proxy.proxeon.net:8000"; $env:HTTPS_PROXY="http://user:pass@proxy.proxeon.net:8000"; $env:TARGET_URL="https://stage.example.com/api/health"; k6 run load-test.jsTip: The discardResponseBodies parameter disables storing response bodies in memory. This reduces the load on the generator itself and helps not to confuse its overload with the proxy limit.
Reading the k6 Report
After the test finishes, k6 outputs a summary. Pay attention to key lines:
- http_reqs — total number of requests and average RPS (in parentheses). This is your throughput.
- http_req_duration — latencies, showing avg, min, med, max, and percentiles p(90), p(95).
- req_errors and http_req_failed — error rate.
- vus — number of virtual users at a given moment.
The thresholds line will show whether your thresholds were met. If there's a cross next to a line, the threshold was violated — meaning at this configuration the limit has already been reached or exceeded.
⚠️ Attention: The target values in stages should be selected for your system. The value 200 VU is just an example. If your target or proxy plan limit is lower, start with more modest steps, e.g., up to 50.
Expected result: The test completes, and you see a summary with RPS, percentiles, and error rate.
Possible issues: If all requests fail, check your proxy variables. If the generator uses 100% CPU — reduce the number of VUs or run the test on a more powerful machine.
✅ Verification: The k6 summary shows a non-zero http_reqs, and the error rate is below your threshold at least at the early steps.
Step 4: JMeter — The Same Scenario with Proxy Settings in the Test Plan
Goal of this step: Build an equivalent test plan in JMeter with a proxy, steps, and metric collection.
Creating a Thread Group
- In JMeter, right-click on Test Plan.
- Select Add - Threads (Users) - Thread Group.
- In the Number of Threads field, set the maximum number of threads, e.g., 200.
- In the Ramp-up period field, set the time to reach full load in seconds, e.g., 540, to replicate the step ramp-up from k6.
- In the Loop Count field, check the Infinite checkbox; we'll set the duration with a timer and scheduler.
Proxy Setup in HTTP Request Defaults
To avoid specifying the proxy in every request, we'll set it once.
- Right-click on Thread Group and select Add - Config Element - HTTP Request Defaults.
- In the opened element, find the proxy server settings tab.
- In the Server Name or IP field (in the Proxy block), enter the proxy host, e.g., proxy.proxeon.net.
- In the Port Number (Proxy) field, enter the port, e.g., 8000.
- In the Username and Password (Proxy) fields, enter your credentials.
Adding the HTTP Request Itself
- Right-click on Thread Group, select Add - Sampler - HTTP Request.
- In the Protocol field, enter https.
- In the Server Name or IP field, enter the target domain, e.g., stage.example.com.
- In the Path field, enter the path, e.g., /api/health.
- In the Method field, leave GET.
Adding Delay Between Requests
- Right-click on HTTP Request and select Add - Timer - Constant Timer.
- In the Thread Delay field, enter 500 milliseconds to replicate the sleep from k6.
Setting Up Metric Collection
- Right-click on Thread Group and add Add - Listener - Summary Report.
- Also add Add - Listener - Aggregate Report — it shows percentiles.
- For long tests, don't use graphical listeners; they eat memory. Instead, save results to a file.
Tip: For heavy tests, run JMeter in non-GUI mode from the terminal. This way the generator spends resources on load, not on rendering graphs.
Example run in non-GUI mode:
jmeter -n -t plan.jmx -l results.jtl -e -o reportHere, -n runs without GUI, -t points to the plan file, -l specifies the raw results file, and -e -o creates an HTML report in the report folder.
Reading the JMeter Report
Open index.html from the report folder. Key metrics:
- Throughput — throughput in requests per second.
- Response Times Percentiles — latency percentile graph.
- Error percentage — error rate.
- Active Threads Over Time — how concurrency grew.
⚠️ Attention: In JMeter, proxy authentication sometimes requires additional Basic authorization setup. If you see mass 407 errors, add an HTTP Authorization Manager element with your proxy credentials.
Expected result: The JMeter HTML report opens and shows throughput, percentiles, and error rate.
✅ Verification: Throughput and percentile values in JMeter are comparable to k6 results at the same steps. Some minor variations are normal.
Step 5: Distinguishing Proxy Limit from Client Limit and Server Limit — Three Checks
Goal of this step: Precisely determine which of the three nodes is the bottleneck.
When you see degradation, it's important to understand its source. Run three control checks.
Check 1: Is Your Client the Bottleneck?
- During the test, open the system monitor on the generator machine.
- Monitor CPU and RAM usage of the k6 or JMeter process.
- Check the open file descriptor limit on Linux with the ulimit -n command.
If the generator's CPU is close to 100% or you've hit the descriptor limit — the client is the bottleneck, not the proxy. Increase the descriptor limit, reduce the number of VUs, or use a more powerful machine.
Tip: A sign of client overload is increasing latency combined with rising generator CPU usage while RPS stays flat. The proxy has nothing to do with it in that case.
Check 2: Is the Target Server the Bottleneck?
- Run a short test directly, without a proxy, by unsetting the HTTP_PROXY and HTTPS_PROXY variables.
- Compare RPS and percentiles with results through the proxy.
If the RPS limit is nearly identical both directly and through the proxy — the bottleneck is the target server. The proxy only adds a small fixed latency.
⚠️ Attention: The direct test should only be done against your own staging environment. Don't load test third-party resources without permission, even for diagnostic purposes.
Check 3: Isolate the Proxy Itself
- Set up a lightweight stub on your staging environment that returns 200 OK with an empty body immediately.
- Run a test through the proxy against this stub.
The stub responds almost instantly, so latency and RPS limits are now determined mainly by the proxy and network. If RPS hits a ceiling exactly on the stub — you've found the proxy limit.
Let's summarize the logic in a simple decision table:
- Generator CPU is high, RPS doesn't grow — client limit.
- Directly and through proxy are the same — target server limit.
- On a fast stub through the proxy, RPS is capped — proxy limit.
Expected result: You can name the specific node that's limiting your chain.
✅ Verification: You've run all three checks and can justify where exactly the bottleneck is.
Step 6: What to Do with the Results — Threads, Pool, and Data Pull Time
Goal of this step: Turn test numbers into practical settings for your real task.
Choosing a Safe Number of Threads
Take the step before the point of degradation. Let's say at 100 VU you got a stable 180 RPS, p95 around 900 ms, and errors below 1%, while at 200 VU RPS didn't grow but p95 spiked to 4 seconds. Then your working point is around 100 VU, and for safety margin, take 80-90.
Calculating the Proxy Pool Size
If one proxy node sustainably handles N parallel connections and you need M simultaneous ones, the minimum pool size equals M divided by N, rounded up, plus margin. A 20-30% margin covers moments when some connections are occupied by slow responses.
Tip: Always build in pool margin. Real responses are slower than a stub, which means connections are held longer and actual concurrency is higher than calculated.
Estimating Data Pull Time
The formula is simple: time in seconds equals total number of requests divided by sustainable RPS. If you need to collect 1,000,000 requests at a sustainable 180 RPS, that's approximately 5556 seconds, or about 1 hour and 33 minutes, not counting pauses and retries.
- Take the sustainable RPS from your working step.
- Divide the total task volume by this RPS.
- Add 15-20% for retries of failed requests and pauses.
Example calculation in pseudocode for clarity:
total = 1000000; rps = 180; base_sec = total / rps; final_sec = base_sec * 1.2;