API Automation Testing Performance Testing QA Automation

Improving API Performance Using k6: From Bottleneck to Stability

Improving API Performance Using k6: From Bottleneck to Stability

When our authentication API began slowing down during peak traffic hours, we knew something was wrong. Customers complained about long wait times after hitting the Login button. Some users even experienced random failures. What started as a “small delay issue” soon turned into a real performance bottleneck that affected the entire product. 

This is the story of how we used k6 to find the root cause, fix the weaknesses, and improve our API performance by almost 40% in two days. 

Project Background

The project was a mid-sized SaaS platform offering a dashboard for business customers. Every user had to log in before accessing the system. The Login API was one of the most frequently called endpoints. On a typical day:  

  • ~15,000 daily logins 
  • Peak traffic during 9–11 AM 
  • SLA required p95 response time < 500ms 

Things ran smoothly until our user base grew. At around 300 requests per minute, the login service began to choke. 

The Performance Problem 

The symptoms were clear: 

  • p95 response time went above 1.2 seconds 
  • Occasional timeouts (especially in the mornings) 
  • High CPU usage on the backend server 
  • Increasing database query time 

We had some monitoring tools, but the data was too scattered. We needed a structured, repeatable way to reproduce the issue. 

That’s when we brought in k6

Why We Chose k6 

We had tried JMeter earlier, but found it: 

  • Heavy to run 
  • Hard to maintain scripts 
  • Not developer-friendly 
  • Difficult to integrate into CI 

k6, on the other hand, was everything we needed: 

  • Simple JavaScript-based scripting 
  • Fast execution 
  • Works in Docker and CI/CD 
  • Easy to set thresholds 
  • Exports metrics to Grafana 
  • Very easy to scale the load 

It felt like using a modern tool designed for modern APIs. 

Designing the Test Scenario

We wanted a realistic simulation of login traffic. So we planned a load model like this: 

export const options = { 
stages: [
{ duration: '1m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '3m', target: 100 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95) < 500'],
http_req_failed: ['rate < 0.01'],
},
};

The script sent login requests with real credentials, validated response structure, and checked status codes. We also added custom metrics for DB latency returned by the API. 

Before Optimization: Test Results 

The results were clear and concerning: 

Metric Result (Before) 
p95 response time 1200ms 
Error rate 5% 
Max CPU usage ~80% 
Throughput Only 60% of expected 
DB query time ~480ms 

Our Grafana dashboard showed sharp spikes in latency during the ramp-up stage. The API could not handle 100 VUs for sustained periods. 

This helped us recreate the exact issue we faced in production. 

Root Cause Analysis: What k6 Helped Reveal 

The k6 tests pointed us toward the real bottlenecks. 

1. Slow database queries

The API performed three separate queries to fetch user details, roles, and settings. This caused unnecessary load on the database. 

2.  Missing database indexes 

The login table had grown large, and searching by email was slow due to lack of proper indexing. 

3. Unnecessary JSON payload

The API returned more fields than required during login, making serialization slow. 

4. Verbose logging inside loops 

Debug-level logs running for each request increased CPU load. 

5. No caching layer 

Every login fetched fresh configuration settings, even though they changed rarely. 

k6’s clear metrics made the degradation pattern obvious. 

 The Fixes Implemented 

We worked on multiple improvements: 

  • Added proper indexing: Searching by email became instantly faster. 
  • Combined queries: We reduced three queries to one optimized JOIN query.
  • Implemented a small Redis cache: Frequently accessed user settings were cached for 5 minutes. 
  • Reduced payload size: We removed unnecessary fields from the response. 
  • Moved logs to async and reduced verbosity: This brought down CPU usage significantly. 

After Optimization: k6 Re-Test Results 

We reran the same k6 script with the same load model. 

Metric Before After 
p95 latency 1200ms 350ms 
Error rate 5% 0% 
Throughput +60% 2× improvement 
CPU usage 80% 50% 
DB query time 480ms 120ms 
DB query time 480ms 120ms 

The difference was dramatic. 

The API easily handled 100 VUs for long durations and passed the threshold: 

  • http_req_duration p(95) < 500ms 
  • http_req_failed < 1% 

Lessons Learned

Here are our key takeaways: 

  • Performance testing should start early. 
    Issues become harder to fix later. 
  • k6 makes it easy for developers and QA to collaborate. 
    Scripts are simple JavaScript functions. 
  • Load tests should run in CI/CD. 
    We now run k6 smoke load tests on every deployment. 
  • Observability is essential. 
    k6 + Grafana gave deep insights. 
  • Small optimizations can create big improvements. 

Conclusion

k6 helped us turn a slow, unreliable login API into a fast and stable service. Its fast scripting, clear metrics, and CI-friendly design made performance testing simple and effective. This case study is one example, but the same approach can be applied to any API or microservice. 

If you work with APIs and want to prevent performance issues before they hit production, k6 is one of the best tools you can add to your workflow. 

aayush-jaiswal

SDET II