How to Simulate Authentic API Load Testing with k6?

- Start with production evidence: Use API logs, analytics, traces, and business data to identify request rates, endpoint distribution, peak periods, and common user journeys.
- Choose the right workload model: Use VU-based executors for concurrent user journeys and arrival-rate executors when requests must continue arriving independently of response time.
- Test workflows, not isolated endpoints: Authentication, browsing, writes, retries, and follow-up requests often create more realistic backend pressure than one repeated GET request.
- Use representative data: Vary users, IDs, payload sizes, filters, and cache-hit patterns without corrupting shared test data.
- Separate checks from thresholds: Checks validate individual responses; thresholds determine whether the overall test passes or fails.
- Observe the complete system: Correlate k6 results with database, cache, queue, container, and application telemetry.
- Run different performance tests: Smoke, average-load, stress, spike, and soak tests answer different engineering questions.
- Never run an uncontrolled test against production: Define ownership, traffic limits, test accounts, cleanup, monitoring, and abort conditions first.
An API can pass unit, integration, and staging tests yet fail when production traffic arrives. The reason is often simple: functional tests verify correctness, while load tests examine what happens when many requests compete for application threads, database connections, CPU, memory, caches, and downstream services.
However, generating thousands of identical requests does not automatically create a realistic load test. Production traffic contains different user journeys, request frequencies, data volumes, authentication states, pauses, retries, and sudden demand changes.
k6 is an open-source performance-testing tool that lets developers model these workloads with JavaScript. It can generate controlled traffic, measure API behaviour, and fail a test automatically when defined performance criteria are violated.
This guide explains how to design k6 tests that reflect real API usage rather than producing arbitrary traffic.
What Makes an API Load Test Realistic?
A realistic load test reproduces the characteristics that create pressure in production. It does not need to reproduce every user action perfectly.
The model should account for:
| Traffic characteristic | Question to answer |
| Arrival rate | How many journeys or requests begin each second? |
| Concurrency | How many users or operations remain active simultaneously? |
| Endpoint distribution | Which API operations receive most of the traffic? |
| User journeys | Which requests happen together and in what order? |
| Data variation | Do requests use different users, records, filters, and payloads? |
| Traffic shape | Is demand constant, gradual, spiky, or seasonal? |
| Cache behaviour | How much production traffic results in cache hits or misses? |
| Geographic origin | Does traffic reach the API through different regions or network paths? |
| Failure behaviour | Do clients retry, time out, or abandon slow operations? |
Device diversity matters primarily when testing the client experience. For protocol-level API testing, the more relevant factors are request behaviour, network location, connection reuse, payload size, and client retry policies.
A test becomes useful when its assumptions can be explained. “Five hundred VUs” means little unless the team knows how that number relates to production concurrency or traffic.
Start With Production Traffic Data
Before writing a k6 script, study how the API is currently used.
Application logs, API gateway metrics, traces, analytics, and database telemetry can reveal:
- Requests per second during normal and peak periods
- Most frequently used endpoints
- Read-to-write ratio
- Response-time percentiles
- Common request sequences
- Payload-size distribution
- Error and retry rates
- Geographic traffic distribution
- Authentication and session behaviour
Suppose production logs show that 65% of traffic reads catalogue data, 20% searches, 10% updates carts, and 5% creates orders. Repeating the order endpoint for every virtual user would produce a very different database and cache workload.
Begin with an approximate model and document its source. As production behaviour changes, update the test rather than allowing an old workload to become a permanent benchmark.
Installing and Setting Up k6
Install k6 using the appropriate method for your development environment.
macOS
brew install k6Windows
choco install k6Linux
sudo apt-get update
sudo apt-get install k6The Linux commands above work only when the appropriate k6 package source is available to the system. Follow the current Grafana k6 installation instructions for your distribution if the package cannot be located.
Verify the installation with:
k6 versionk6 scripts use JavaScript syntax, but they do not run inside Node.js. Node-specific packages and built-in modules are not automatically available.
Understand the Core k6 Concepts
A few terms appear throughout k6 tests:
| Concept | Meaning |
| Virtual user | An independent execution context running a test function |
| Iteration | One complete execution of the assigned scenario function |
| Scenario | A workload definition with its own function and scheduling model |
| Executor | Controls how VUs or iterations are scheduled |
| Stage | Changes the target VUs or arrival rate over time |
| Check | Validates an individual response or condition |
| Threshold | Defines aggregate pass-or-fail performance criteria |
| Metric | A measurement such as duration, failures, requests, or iterations |
The executor is one of the most important choices because it determines whether k6 models concurrent users or arriving work.
Closed vs Open Workload Models
k6 supports both closed and open workload models.
Closed Model
In a closed model, each VU begins its next iteration only after its current iteration finishes. When the API slows down, iterations take longer, and the generated request rate may fall.
This is suitable when modelling a fixed population of users who wait for each operation before continuing.
Open Model
In an open model, new iterations begin at a defined rate independently of how long previous iterations take. k6 provides constant-arrival-rate and ramping-arrival-rate executors for this purpose.
This is useful when modelling external work that continues to arrive even as the API slows, such as webhooks, orders, events, or requests entering through a gateway.
| Requirement | Suitable model |
| Simulate a fixed number of active users | Closed |
| Reproduce a target number of arrivals per second | Open |
| Include user think time between actions | Usually closed |
| Prevent slow responses from reducing incoming load | Open |
| Find concurrency under a known traffic rate | Open |
| Model long user sessions | Usually closed |
Grafana’s documentation explains that a closed workload can understate pressure when slower responses cause the request rate to decline, a measurement problem sometimes called coordinated omission.
Choosing an executor based only on convenience can produce misleading results even when the script itself is technically correct.
Designing a Basic k6 Scenario
The following example uses VU stages:
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '20s', target: 10 }, // Ramp-up from 0 to 10 virutal Users over 20 seconds
{ duration: '10s', target: 0 }, // Ramp-down to 0 virtual users over 10 seconds
],
};
export default function () {
http.get('https://your-api-endpoint.com');
sleep(1);
}This script ramps from zero to ten VUs over 20 seconds and then ramps back to zero over ten seconds. It does not maintain ten VUs for five minutes; the original explanation incorrectly described a stage that is not present in the configuration.
Each active VU repeatedly sends the GET request and waits for one second. Because this is a closed workload, API response time affects how many iterations each VU can complete.
This is appropriate as a small demonstration or smoke test. It is not yet an authentic production workload because it covers only one endpoint, one request type, and one user behaviour.
Model User Journeys, Not Just Requests
Production users usually perform related actions.
Ensure API Stability with Expert QA Testing
F22 Labs tests your APIs for speed, reliability, and scalability. Our QA experts make sure your system performs smoothly under real traffic.
An e-commerce journey might authenticate, browse products, search, open product details, update a cart, and occasionally check out. A SaaS user might load a dashboard, request reports, update records, and poll a job-status endpoint.
These sequences affect the system differently from isolated traffic. Authentication may access an identity service, catalogue reads may hit caches, search may query a separate index, and checkout may create database locks or call payment providers.
Use separate k6 scenarios or functions for important user groups. Assign their workload proportions using production evidence rather than splitting traffic equally by default.
Keep the model focused. Simulating every rare action makes scripts difficult to understand without necessarily improving the test.
Add Realistic Think Time Carefully
Human users pause between actions. The following example introduces a random delay:
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('https://your-api-endpoint.com');
sleep(Math.random() * 5); // Random think time between 0 to 5 seconds
}This produces a uniform random pause between zero and five seconds. Real user timing may follow a different distribution, but variation is generally more representative than forcing every VU to pause for exactly the same duration.
Think time is appropriate in VU-based user journeys. Do not add it simply to achieve a target request rate. Arrival-rate executors already schedule iterations according to rate and timeUnit; Grafana advises against adding sleep() merely to pace those scenarios.
Use Representative Test Data
Sending identical requests can create unrealistic cache behaviour, lock contention, or duplicate-record failures.
Parameterize values such as:
- User accounts
- Product and record IDs
- Search terms
- Pagination
- Request payload sizes
- Geographic or tenant identifiers
- Valid and invalid inputs
Test data must also support concurrency. If several VUs attempt to update the same account or create the same resource, the test may measure artificial conflicts rather than production behaviour.
Decide whether data can be reused, must be partitioned by VU, or needs to be created and cleaned up around the test.
Avoid generating all data through the system under test during the measured period unless data creation is itself part of the workload. Setup traffic can distort the results.
Handle Authentication Without Testing Only Login
If every iteration logs in before calling the target endpoint, authentication can become the dominant workload even when production clients normally reuse tokens.
Model the actual session lifecycle. Depending on the system, this may mean authenticating once per VU, using pre-generated test tokens, refreshing tokens at realistic intervals, or modelling login as a separate traffic segment.
Never place production credentials directly in the script. Pass secrets through an appropriate environment or secret-management mechanism and ensure that logs do not expose tokens or personal data.
Validate Responses With Checks
Fast error responses can make latency metrics look excellent while the API is failing.
A meaningful test should confirm that responses are functionally valid. k6 checks can verify status codes, response fields, headers, or other expected conditions.
Checks record success and failure rates, but a failed check does not automatically fail the complete k6 run. To enforce a limit, combine the check metric with a threshold.
This distinction is important:
- A check asks whether an individual response behaved correctly.
- A threshold asks whether the aggregate test result remained within an acceptable limit.
Define Performance Thresholds
Thresholds turn expectations into pass-or-fail criteria.
export let options = {
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
},
};This threshold passes only when the 95th-percentile HTTP request duration is strictly below 500 milliseconds.
It does not mean the test fails only when 95% of requests exceed 500 milliseconds. It means that approximately 95% of observed request durations must fall at or below the percentile boundary represented by a value under 500 milliseconds.
Grafana defines thresholds as pass-or-fail conditions evaluated against test metrics. They can be used to encode service-level objectives and CI performance requirements.
Latency should rarely be the only threshold. A useful test may also enforce limits for request failure rate, check success, dropped iterations, or endpoint-specific latency.
Choose thresholds from product expectations and existing service objectives. A universal p(95)<500 target is not appropriate for every API operation.
Segment Results by Endpoint and Operation
A global latency percentile can hide important failures.
If 95% of traffic reaches a fast health endpoint and 5% reaches a slow checkout endpoint, the combined p95 may appear acceptable even though checkout users experience severe delays.
Use tags, groups, or separate scenarios to distinguish:
- Endpoint
- Operation
- User journey
- Expected response
- Region
- Cache state
- API version
k6 allows requests, checks, thresholds, and custom metrics to be categorized with tags.
Keep tag values bounded. Using unique user IDs or request IDs as metric tags can create excessive time-series cardinality in monitoring systems.
Running the Load Test
Execute the script from the command line:
k6 run your-test-script.jsBegin with a small smoke test. Confirm that authentication works, requests are correct, test data remains valid, and the load generator is not already resource-constrained.
Increase traffic incrementally after validating the script and environment. A large test with an incorrect request model can waste resources or damage shared data without producing useful results.
Read the Right k6 Metrics
k6 records several built-in HTTP measurements.
| Metric | What it reveals |
http_req_duration | Total request duration |
http_req_waiting | Time waiting for the first response byte |
http_req_connecting | Time spent establishing TCP connections |
http_req_tls_handshaking | TLS negotiation time |
http_req_failed | Rate of failed HTTP requests |
http_reqs | Total requests and request rate |
iteration_duration | Duration of the complete user iteration |
vus | Currently active VUs |
dropped_iterations | Scheduled iterations k6 could not start |
Response averages are often insufficient because a small number of very slow requests can be hidden. Examine percentiles such as p90, p95, and p99 alongside error rates and throughput.
For arrival-rate scenarios, dropped iterations deserve special attention. They may indicate that k6 lacks enough VUs or load-generator resources to maintain the requested arrival rate. They can also appear when the system slows enough that available VUs remain occupied.
Correlate k6 Results With System Telemetry
k6 can show when response time increased, but backend telemetry explains why.
Monitor the application and its dependencies during the same test window:
| Layer | Useful signals |
| Application | Request duration, errors, thread or event-loop pressure |
| Containers | CPU throttling, memory, restarts and replica scaling |
| Database | Query latency, connections, locks and slow queries |
| Cache | Hit rate, memory, evictions and command latency |
| Queues | Depth, publish rate and consumer lag |
| Network | Connection errors, saturation and cross-region latency |
| Dependencies | Rate limits, timeouts and downstream failures |
k6 metrics can be streamed to compatible backends and visualized in Grafana. Combining load-generator and system metrics on the same timeline helps connect a latency spike with events such as database saturation or cache eviction.
Without backend telemetry, the result may be limited to “the API became slow at 800 requests per second.” With telemetry, the team may learn that the database connection pool saturated first.
Run the Right Type of Performance Test
One large load test cannot answer every question.
| Test type | Purpose |
| Smoke test | Confirms the script works under minimal traffic |
| Average-load test | Evaluates expected day-to-day demand |
| Stress test | Raises load beyond normal levels to find degradation |
| Spike test | Examines sudden traffic increases and recovery |
| Soak test | Runs sustained load to reveal leaks or resource accumulation |
| Breakpoint test | Continues increasing pressure to identify system limits |
Run a smoke test before every larger execution. It is much cheaper to detect an invalid token or destructive data mistake with one VU than with hundreds.
Test in a Production-Representative Environment
A small staging environment cannot be expected to produce production capacity numbers. Its results may still be useful for regression testing, but they should not be presented as production limits.
Match the factors that materially influence performance:
- Application configuration
- Database engine and schema
- Cache topology
- Network path
- Container limits
- Autoscaling policy
- Data volume
- External dependencies
- Observability agents
If exact parity is impossible, document the differences. This allows readers to understand what the result proves and what remains uncertain.
Ensure API Stability with Expert QA Testing
F22 Labs tests your APIs for speed, reliability, and scalability. Our QA experts make sure your system performs smoothly under real traffic.
Run Production Tests Safely
Production testing provides realistic infrastructure and data distribution, but it also carries the highest risk.
Before sending load to production, establish:
- Explicit authorization and ownership
- A controlled traffic ceiling
- Test accounts and identifiable traffic
- Data cleanup procedures
- Monitoring coverage
- Abort criteria
- Incident communication
- Protection for real users
- Awareness of third-party usage charges
Avoid destructive endpoints unless their effects are isolated and reversible. Coordinate with infrastructure, database, security, and support teams before execution.
Never treat “the test tool can generate this load” as permission to send it.
Common k6 Load-Testing Mistakes
1. Choosing VUs Without Knowing the Target Traffic
VU count is not equivalent to requests per second. Iteration duration, think time, and the number of requests per journey determine the resulting throughput.
2. Repeating One Endpoint
A single repeated GET request often creates an unrealistically cache-heavy workload and ignores database writes, authentication, and dependent services.
3. Ignoring Functional Failures
Latency looks artificially low when the API quickly returns errors. Validate important responses and enforce an error-rate threshold.
4. Using Only Average Response Time
Averages hide long-tail latency. Study percentiles and endpoint-specific results.
5. Testing the Load Generator Instead of the API
CPU, memory, file descriptors, or network limits on the k6 machine can cap traffic. Monitor the load generator and consider distributed execution for larger tests.
6. Making Every Request Unique
Unlimited random values can prevent realistic caching and create unmanageable metrics. Data diversity should resemble production, not eliminate every cache hit.
Comparing Uncontrolled Test Runs
Results cannot be compared confidently when the environment, data state, deployment version, or traffic model changes between runs.
A Practical Testing Workflow
A sustainable k6 process moves from evidence to repeatable tests.
Start by collecting production traffic and service objectives. Convert the most important user journeys into scenarios and select executors that match how work arrives.
Validate the script with a smoke test, then establish an average-load baseline. Add stress, spike, or soak tests according to the risks of the system.
During each run, collect k6 and backend telemetry together. Record the application version, infrastructure configuration, dataset state, scenario settings, and threshold results so future tests remain comparable.
Finally, automate smaller regression tests in CI while reserving large capacity tests for controlled environments and planned execution windows.
Frequently Asked Questions
What is k6 used for?
k6 is used to generate controlled workloads and measure the performance of APIs, web applications, browser journeys, WebSockets, and other supported protocols. It can enforce pass-or-fail thresholds in automated pipelines.
What is the difference between VUs and requests per second?
VUs represent active execution contexts. Requests per second measure generated throughput. One VU can send several requests per iteration, and slower responses can reduce throughput in a closed workload.
Should I use VUs or an arrival-rate executor?
Use VU-based executors to model a fixed population of users completing journeys. Use arrival-rate executors when work should continue entering the system at a defined rate independently of response time.
Are k6 checks the same as thresholds?
No. Checks record whether individual conditions pass. Thresholds evaluate aggregate metrics and determine whether the complete test passes or fails.
Can k6 test authenticated APIs?
Yes. Tests can authenticate users, reuse tokens, refresh credentials, and send authorization headers. The authentication lifecycle should match production behaviour rather than logging in before every request automatically.
Can k6 run distributed load tests?
Yes, but distributed execution requires appropriate coordination through solutions such as Grafana Cloud k6 or the k6 Operator. Metrics and thresholds must be interpreted correctly across load-generator instances.
Should load tests run in production?
Only with explicit authorization, controlled scope, monitoring, abort conditions, isolated data, and protection for real users. A production-like environment is safer for most routine testing.
Which latency percentile should I monitor?
p95 is commonly used, but p90, p95, and p99 reveal different parts of the latency distribution. Choose percentiles based on service objectives, request volume, and the importance of long-tail user experiences.
Conclusion
Realistic API load testing with k6 is not about producing the largest possible number of requests. It is about creating a workload that reflects how traffic arrives, which journeys users follow, which data they access, and how the system behaves when demand changes.
Start with production evidence, choose an appropriate open or closed workload model, and represent the most important endpoint mix. Add realistic data, authentication, checks, and enforceable thresholds.
During execution, observe the complete system, not only k6 response times. Database connections, cache behaviour, queue depth, container resources, and downstream services often reveal the actual bottleneck.
A useful load test should answer a specific engineering question and be repeatable enough to detect change. When k6 scenarios evolve alongside production traffic, performance testing becomes an ongoing reliability practice rather than a final check before launch.



