Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Consulting Services Hire Book Consulting

Performance Testing Services in India

Load, stress, soak, spike and breakpoint testing from an India-based engineering team, built for CTOs, founders and engineering managers running teams in the US, UK, Canada, Australia and New Zealand. We model your real traffic, find where the system stops coping, and name the component that gives out first instead of handing you a graph and wishing you luck.

The Bug You Only Have When People Are Watching

Every functional test passes. The staging environment is quick. Then the campaign email goes out at nine in the morning, traffic triples for twenty minutes, and checkout starts returning gateway timeouts while CPU on the application servers sits at thirty percent. Nobody can explain it, so somebody restarts something, the traffic drops off, and the incident closes as resolved with no cause recorded.

That is the shape of the problem performance testing services in India get hired to solve. Not a slow page in isolation, which any developer can profile in an afternoon, but the class of failure that only appears when concurrency, data volume and time all rise together. Those three variables are precisely the ones a normal test suite holds constant.

The cost is easy to describe and hard to look at. An hour of degraded checkout during a promotion is revenue you will never recover, because the customer went somewhere else and their card is already saved there. Beyond the lost orders there is the engineering time: three or four senior people pulled onto an incident channel for a day, then a week of speculative optimisation aimed at whatever component someone blamed in the heat of it. We have seen teams spend a month adding a caching layer to a service that was never the constraint, because nobody measured before they built.

The other cost is slower and worse. Once a system has surprised people twice, the organisation stops trusting it. Releases get batched because deploying feels risky. Marketing stops planning campaigns without warning engineering a fortnight in advance. Capacity decisions get made by adding a zero to the instance count, which works, in the sense that setting money on fire also produces heat.

What you actually want is boring: a number for how much traffic the system takes before response times cross your tolerance, a name for the component that gives out first, and a repeatable way to check whether last week's change made it better or worse. That is what this work produces.

Load, Stress, Soak, Spike and Breakpoint: What Each One Actually Tells You

These get used interchangeably in requirements documents and they are not interchangeable at all. Each answers a different question, and running the wrong one is how a team ends up confident about the wrong thing. A useful engagement runs most of them, in an order, because they build on each other.

Load test: does it hold at the traffic you expect?

Drive the system at your realistic peak, hold it there for long enough to be meaningful, usually thirty to sixty minutes, and watch the latency distribution and error rate. This is the test everyone means when they say load testing, and on its own it tells you the least, because it only confirms that a level you already survive is survivable. Its real value is as the reference run: everything else gets compared against this baseline, and a regression is measured as movement away from it.

The mistake here is running it for four minutes. Four minutes is shorter than most autoscaling reaction times, shorter than a JIT warm-up on a JVM service, and far shorter than the interval at which a garbage collector does its expensive work. A four minute run measures your system in a state it is almost never in.

Stress test: what happens on the way past the limit?

Push beyond expected peak until behaviour changes, then keep watching. The question is not whether it breaks, because everything breaks at some level. The question is how. A system that sheds load cleanly, returns 503 quickly, keeps its health checks honest and recovers within a minute of the pressure lifting is in decent shape. A system that queues everything, holds connections open, times out at the load balancer, marks healthy instances unhealthy and then takes twenty minutes to come back after traffic normalises has a design problem that no amount of extra capacity will fix.

The recovery half of a stress test is the part most teams skip and the part we find most informative. Stop the load and time how long it takes to be well again. If the answer is longer than a few minutes, you have a system that turns a traffic spike into an outage.

Soak test: what leaks or drifts over hours?

Moderate, realistic load held for six, twelve or twenty-four hours. Nothing else finds this class of bug. Heap creeping upward across hours until a JVM spends its life in full garbage collections. A connection leak that only exhausts the pool after forty thousand requests. An unbounded in-memory cache with no eviction policy that is fine on Monday and out of memory by Wednesday. A log file filling a disk. A database table whose query plan flips once its size crosses a threshold the optimiser cares about.

Ask any team with a nightly restart cron why it exists and you will usually get a shrug. That cron is a soak test failure that somebody worked around instead of diagnosing. Soak runs are also the easiest part of this work to hand to a team in a different timezone, since they need a quiet environment and nobody watching, which is exactly what your night provides.

Spike test: how fast can it react?

Jump from low traffic to very high in seconds rather than ramping. This tests the reaction time of everything elastic in your stack, and the results are frequently unpleasant. Container autoscaling takes time to observe a metric, decide, schedule a pod and pass a readiness check. New instances start with cold caches, cold connection pools and, on the JVM, interpreted bytecode that has not been compiled yet, so they are slower than the ones already running while simultaneously being handed a share of the worst traffic of the day.

Our experience is that spike failures are usually warm-up failures rather than ceiling failures. The system could handle the volume if it had five minutes of notice. The fix is often pre-warming, keeping headroom instead of scaling on the edge, or a queue that absorbs the burst, and none of those get considered if the only test you ever ran was a gentle ramp.

Breakpoint test: where is the ceiling, precisely?

Ramp continuously until the system stops coping, and record the exact point at which throughput stops rising with offered load. That inflection is the number worth knowing. Below it, adding traffic adds throughput. Above it, adding traffic adds only latency and errors, because a queue somewhere has started growing faster than it drains.

Give this number to your product and marketing people. The useful framing is not the raw request rate, which nobody outside engineering can act on, but the headroom multiple: we currently take about three times our busiest hour before checkout latency crosses two seconds. That sentence changes planning conversations. A dashboard does not.

Where the front end fits

Protocol-level load testing measures your servers. It does not measure what a real browser does with the response: parsing, rendering, layout shifts, hydration cost, blocking third-party scripts. Both matter, and they are different measurements taken with different tools. Browser-level checks on Largest Contentful Paint and Interaction to Next Paint are worth running, and driving thousands of real browsers is an expensive way to load a server. We usually run protocol-level load for capacity work and a small number of browser sessions alongside it to catch the case where the API got faster and the page did not.

Why Does Your Average Response Time Lie to You?

Because it was never designed to describe a distribution with a long tail, which is exactly what web latency is. Response times are not a bell curve. They are a dense cluster of fast responses with a thin, long tail of slow ones, and the mean sits comfortably inside the cluster while telling you nothing about the tail.

Take a service averaging 200 milliseconds. That average is equally consistent with a system where every request takes roughly 200 milliseconds and one where forty-nine requests in fifty take 40 milliseconds and the fiftieth takes eight seconds. The second system has a serious problem. The dashboard shows the same green number for both.

Percentiles fix this, and they matter more than people expect because they compound across a call graph. Dean and Barroso made this argument in The Tail at Scale in Communications of the ACM back in 2013, and the arithmetic is easy to check yourself. If a single page assembles data from twenty backend calls and each call has a p99 of one second, the chance that all twenty come back fast is 0.99 to the twentieth power, roughly 82 percent. So around one page view in five includes at least one one-second call. Your service-level p99 looks respectable and one user in five is waiting. Fan-out turns a rare event into a common one.

There is a second trap, subtler and more damaging, and it lives inside your load testing tool. Gil Tene named it coordinated omission. A load generator using a fixed number of virtual users with think time only sends its next request after the previous response arrives. When the system stalls for two seconds, that user does not issue the requests it should have issued during the stall. Those requests are simply missing from the results, and they are precisely the ones that would have recorded terrible latency. The tool measures how fast the system was when it was answering and quietly omits the period when it was not.

The consequence is not a rounding error. A run affected by coordinated omission can report a p99 several times better than reality. The defence is to model an open workload where requests arrive on a schedule regardless of whether earlier ones have finished, which is how real users behave: they do not politely wait for your server to recover before clicking again. k6 has constant-arrival-rate and ramping-arrival-rate executors for this. Gatling injection profiles do it with constantUsersPerSec and rampUsersPerSec. Locust has constant_throughput. In JMeter you need the Concurrency Thread Group and the Throughput Shaping Timer from the plugins ecosystem, because the stock thread group is a closed model. wrk2 exists specifically because Tene rewrote wrk to correct for this.

One more thing about percentiles, since it produces confidently wrong reports. You cannot average percentiles. Taking the p99 from each of ten load generators and averaging them does not give you the p99 of the run. Percentiles have to be computed from the merged distribution, which is why HdrHistogram and the histogram types in k6 and Prometheus store buckets rather than summaries. If a report shows you an averaged p99 across regions or shards, the number is not wrong by a little.

What we report, on every run, is p50, p95, p99, maximum, throughput actually achieved against throughput offered, and error rate broken down by status code and by error type. The gap between offered and achieved load is often the most revealing line in the whole report, because it is the moment the system started refusing work.

Workload Modelling: Using Your Real Traffic Instead of Inventing Numbers

Most disappointing load tests fail here, before a single script is written. Somebody decides the test should simulate a thousand concurrent users, a number that came from a meeting rather than from data, and the whole exercise inherits that fiction. The result passes, everyone relaxes, and the system falls over at a traffic pattern nobody modelled.

Start with the logs, not the whiteboard

Your access logs, load balancer logs or analytics already contain the answer. We pull the busiest hour of the last twelve months, then break it down: requests per second per endpoint, the ratio between read and write operations, the distribution of session lengths, how many requests a typical session makes, and the proportion of traffic that is authenticated. That last one matters more than it sounds, because anonymous traffic usually hits cache and logged-in traffic usually hits the database.

The output is a mix, not a total. A test that sends eighty percent of its traffic to the product listing page when production sends eighty percent to search is testing the wrong system. We have seen a rewritten test change nothing about the load level and move the bottleneck from the web tier to the database purely by correcting the endpoint mix.

Think time, and why omitting it produces fiction

Real people read. They pause between actions, get distracted, open a second tab, come back. A script with no think time hammers the next request the microsecond the last one returns, so a hundred virtual users generate the request rate of several thousand real ones, all of it arriving in a pattern no human population produces.

The damage runs in two directions. Without think time, a modest virtual user count looks like a catastrophic result and the team panics over a number that means nothing. Meanwhile the concurrency being simulated is far lower than the equivalent real traffic, so connection pool and session memory pressure are understated. You get a test that is simultaneously too harsh on throughput and too gentle on concurrency, which is an impressive way to be wrong twice.

Think time should also be distributed, not constant. Every user pausing exactly three seconds creates a synchronised wave that produces artificial peaks and troughs. A randomised pause across a realistic range, or a normal distribution around a mean, gives an arrival pattern that behaves like a crowd. Better still, stop thinking in virtual users at all and specify arrival rate directly, then let the tool work out how many users it needs to sustain it. That is the open model, and it is closer to how traffic actually reaches you.

Test data that does not lie for you

A script that requests the same product ID ten thousand times will report excellent numbers, because after the first request everything is in cache. All you have measured is the cache. Parameterising with a realistic spread of identifiers is not optional, and the spread has to match production: if ten percent of your catalogue receives ninety percent of the views, the test data should be skewed the same way, or you will either overstate the cache hit rate or understate it.

Write paths need more care again. A load test that creates orders needs enough distinct users and payment fixtures to avoid every virtual user updating the same row, which turns the test into a lock contention benchmark. It also needs a reset strategy, because a test that inflates a table by two million rows changes the behaviour of the next run and you lose your baseline. And if you are considering a copy of production data, the anonymisation and the legal basis for holding it are questions for your counsel and your data protection people before we touch it, not after.

Correlation, and the thing that breaks every recorded script

Recorded scripts almost never replay unmodified. Anti-CSRF tokens, session identifiers, dynamic form fields, signed URLs and one-time nonces are unique per session, so a replayed script sends yesterday's token and receives a polite 403 that the tool cheerfully records as a fast response. This is the single most common reason a load test reports beautiful numbers while proving nothing.

The defence is a response check on every step. Validate the status code, and also validate something in the body that only appears on success, then treat a failed check as a failed request in the results. It is dull to build and it is the difference between a result and a graph.

Environment Parity and the Scaled-Down Copy Trap

The most common request we get is to test a staging environment sized at a fraction of production and extrapolate. It is understandable, it is cheaper, and it is where a lot of performance testing quietly stops being useful. Systems under load are not linear, so a tenth of the hardware does not give you a tenth of the answer.

Why the small copy behaves differently in kind, not just degree

Consider the database buffer pool. Staging holds a 4 GB dataset with 8 GB of memory, so effectively everything is in memory and disk reads never happen. Production holds 900 GB with 64 GB of memory, so the working set misses constantly and every query pays for IO. Those are not the same system running at different speeds. They have different bottlenecks entirely, and no scaling factor connects them.

Instance types add their own trap. Burstable cloud instances, the T family on AWS and the equivalents elsewhere, accumulate CPU credits when idle and spend them under load. A test on a burstable instance looks excellent for the first fifteen minutes, then collapses when the credits run out, and the team either panics at an artefact or, worse, runs only short tests and never sees it. Production on a non-burstable instance family behaves nothing like it.

The list of differences that change conclusions is longer than most people expect: connection limits set lower in staging, a single availability zone instead of three so cross-zone latency vanishes, no CDN in front, autoscaling disabled to save money, one application instance rather than twelve so no load balancer distribution effects, debug logging left on, a different database version, and query plans that differ because table statistics on a small dataset lead the optimiser somewhere else.

What we do instead

Where a production-like environment can be stood up for a window, that is the strongest option, and infrastructure as code makes it far cheaper than it used to be: create the environment, restore an anonymised dataset of realistic volume, run for a day, destroy it. You pay for a day of production-sized infrastructure rather than for keeping a second production running permanently.

Where that is not possible, we say so in the report rather than hiding it. A scaled environment still gives you relative measurements, which is genuinely valuable: this release is fifteen percent slower than last release under identical conditions. It does not give you absolute capacity numbers, and any report claiming otherwise is guessing. Component-level testing helps here too, since a single service tested at production sizing tells you more than an entire stack tested at a tenth.

Testing in production is the blunt end of this spectrum, and it is more common than the industry admits. It requires real discipline: a low-traffic window, a kill switch, synthetic traffic tagged so analytics and billing exclude it, test accounts, third-party integrations pointed at sandboxes, and agreement with whoever is on call. Done carefully it removes environment parity from the argument entirely, because there is nothing to extrapolate from. Ongoing capacity work of this kind, along with SLO definition and headroom planning, is covered by our site reliability engineering services in India, and it is the natural place for this work to continue once the first round of bottlenecks is cleared.

The load generator is part of the environment

Half the strange results we get asked about are the client's fault, not the server's. A single generator saturating its own CPU on TLS handshakes. Ephemeral port exhaustion after a few tens of thousands of connections because the operating system has around 28,000 ports available by default and TIME_WAIT holds them. File descriptor limits left at the default. DNS resolution repeated per request instead of cached. A generator in a different continent from the target, adding a hundred milliseconds of round-trip to every measurement.

So we calibrate before we conclude. Run the generator against a trivial static endpoint and confirm it can produce well above the target rate with low latency and no errors. Watch generator CPU, memory and socket counts during the real run. If the client is anywhere near its limits, the numbers are about the client.

What a Performance Testing Engagement Includes

Scope varies with what you already have. A team with good observability and a stable staging environment needs something quite different from one testing for the first time. The following is the full set, and the assessment decides how much of it applies.

A workload model you can argue with

A written model of what we are simulating and why: peak hour derived from your traffic data, endpoint mix with percentages, session shape, think time distribution, data volumes, and the growth factor applied. You review this before any script runs. If it is wrong, everything downstream is wrong, and it is far cheaper to argue about it on a page than to discover it three weeks later.

Scripts that live in your repository

Test scripts committed to your version control, parameterised, with response validation on every step, written to be read and edited by your engineers rather than by us. If the engagement ends and the tests become unmaintainable, you have rented a report instead of building a capability.

Baseline, peak, stress, soak and breakpoint runs

The measured sequence described above, each run recorded with the full latency distribution, throughput achieved against offered, error breakdown, and the correlated system metrics captured over the same window. Raw results are kept so a later run can be compared against them properly rather than against a screenshot.

A bottleneck analysis that names components

This is the deliverable that matters. Not a statement that response time degrades above 400 requests per second, which you could have discovered yourself, but that response time degrades above 400 requests per second because the HikariCP pool is capped at 20 while the average query holds a connection for 90 milliseconds, so the pool saturates at roughly 220 queries per second and everything above that queues. Then the evidence: pool wait time, database active connections, the slow query, the thread dump. A finding without evidence is an opinion.

Prioritised recommendations with honest effort estimates

Each finding gets an expected improvement and a rough cost, split between configuration changes measured in hours, code changes measured in days, and architectural changes measured in months. We are explicit about which ones we can validate quickly and which need a redesign to prove, and about the ones where the right answer is to accept the limit and provision for it.

Retest and a regression harness

After fixes land, the same tests run again under identical conditions so the improvement is measured rather than assumed. The short version of the suite gets wired into your pipeline with thresholds, so the next regression is caught by a build rather than by a customer.

Handover documentation

How to run the tests, how to read the results, what the thresholds mean and why they are set where they are, what environment assumptions the numbers depend on, and the known limitations of the setup. If your team cannot run and interpret these tests without us, the engagement has not finished.

Which Load Testing Tool Should You Use?

Tool choice matters less than workload modelling, and teams spend more time on it anyway. Any of the four below will find your bottleneck if the model is right, and none of them will if it is wrong. That said, the trade-offs are real, so here they are without the vendor gloss.

k6

A Go binary that runs test scripts written in JavaScript. Our default for most engagements. Low memory per virtual user, so a single modest machine drives high concurrency. Tests are plain files that review well in a pull request. The thresholds feature is the reason it wins for pipeline work: you declare that p95 must stay under 500 milliseconds and the error rate under one percent, and the process exits non-zero when it does not, which is exactly the contract a CI job wants. The arrival-rate executors give you an open model without extra plugins.

What you give up: the JavaScript runtime is not Node, so npm packages generally need bundling and native modules will not work at all. Protocol coverage beyond HTTP, WebSocket, gRPC and a handful of others depends on xk6 extensions, which means compiling your own binary. Distributed execution across many machines needs the Kubernetes operator or the commercial cloud product rather than being built into the open-source binary.

Apache JMeter

The oldest of the four and still the answer to certain questions. Nothing else matches its protocol coverage: JDBC straight to a database, JMS, LDAP, FTP, SMTP, plus a plugin ecosystem two decades deep. If your load has to include a message queue or a direct database workload alongside HTTP, JMeter often does it with no custom code.

What it costs you: a thread per virtual user, so memory and context switching become the constraint at high concurrency and you end up distributing across machines earlier than with the others. The .jmx test plan is XML, effectively unreviewable in a diff, which pushes teams back into the GUI. And the GUI must not be used to generate load, only to build plans, since running from it distorts the results. The stock thread group is a closed model, so for open workloads you need the Concurrency Thread Group and Throughput Shaping Timer plugins.

Gatling

Scala underneath, with a DSL available in Scala, Java and Kotlin. Non-blocking IO throughout, so like k6 it sustains high concurrency on modest hardware. The generated HTML report is the best of the four out of the box, and the DSL expresses injection profiles clearly, which is a genuine advantage when you are describing ramps, plateaus and spikes in one scenario. If your team is already on the JVM, it fits their tooling and their debugger.

The friction: the DSL takes longer to learn than a JavaScript file, particularly the Scala variant, and there is a compile step in the loop. Clustering multiple load injectors is a commercial feature rather than part of the open-source distribution, which matters if you need enormous scale.

Locust

Python, gevent-based, tests written as ordinary Python classes. The best choice when your test needs real logic, custom authentication flows, or reuse of your existing Python client libraries, because you are writing plain Python with the whole ecosystem available. Distributed mode is simple: one master, many workers, and it works.

The constraints come from Python. The GIL means one worker process saturates one core, so serious load means running many workers and planning for it. Precise request-rate control was historically weaker than the others, though constant_throughput and constant_pacing address the common cases. And because tests are arbitrary Python, a slow test function silently becomes part of what you are measuring, so keeping the script cheap matters more here than elsewhere.

The specialists worth knowing about

wrk2 for a small, precise, constant-rate HTTP benchmark with correct latency recording, which is often the right tool for measuring one endpoint properly. Vegeta for the same reason with a friendlier command line and a Go library. Artillery when a YAML scenario is genuinely enough and nobody wants to write code. Apache Bench for almost nothing at this point, since it is single-threaded and its results are routinely misread. On the front end, Lighthouse and browser automation measure rendering rather than server capacity, which is a different question with a different answer.

One rule we apply regardless of tool: the tool must be able to record and export the full latency distribution, not a summary. If all you can get out of it is an average and a maximum, you cannot do the analysis, and the tool has made the decision for you.

From a Slow Number to a Named Bottleneck

A load test on its own produces one fact: the system is slow above some level. Turning that into an action requires correlating the run against what every layer was doing at the same moment, which is why we ask for observability access before we ask for an environment. Without it we are outside the building describing the smoke.

Two frameworks organise the search well. Brendan Gregg's USE method walks every resource and checks utilisation, saturation and errors, which is the right lens for infrastructure. The RED method, popularised by Tom Wilkie, checks rate, errors and duration per service, which is the right lens for the application. Between them they cover most of what you need to look at, in an order, rather than by intuition.

CPU: usually innocent, occasionally guilty

High CPU is the bottleneck everyone assumes and rarely the one they have. When it is real, a profiler names it in minutes: async-profiler or Java Flight Recorder on the JVM, py-spy for Python, pprof for Go, perf underneath everything. A flame graph makes the answer visual, and the answer is usually less exotic than expected. Serialisation. Regular expression backtracking. A password hashing round count copied from a blog. Logging at debug level in production. Encryption on a path that did not need it.

Watch run queue length as well as utilisation. A machine at sixty percent CPU with a run queue consistently above its core count is saturated even though the percentage looks comfortable, and the percentage is what your dashboard is showing you.

IO and network: the wait nobody graphs

Disk saturation shows up as IO wait and rising device queue depth, most often from a database on undersized storage or a log volume that has run out of provisioned IOPS. Cloud block storage has a burst allowance that behaves like the CPU credit problem: fine for the first ten minutes, terrible afterwards, which makes short tests actively misleading.

Network problems are frequently about round trips rather than bandwidth. A service making four hundred sequential calls to a database three milliseconds away spends 1.2 seconds doing nothing but waiting, and no amount of extra capacity on either end changes that. This is why chatty code that was fine on a single machine becomes a crisis after a migration splits the tiers apart.

Locks and contention: the reason more cores did not help

The signature is throughput that flatlines while CPU stays moderate and latency climbs. Somewhere a shared resource is being serialised: a synchronised block, a global mutex, a distributed lock held across a network call, a database row that every transaction touches, or an inventory counter in a flash sale. Thread dumps taken during the plateau, three of them thirty seconds apart, usually make the culprit obvious because the same stack frame appears in every dump.

The distinctive property of a contention bottleneck is that scaling out makes it worse. Adding instances increases the number of contenders for the same lock, so throughput can actually fall as you add capacity, which is a genuinely confusing thing to watch on a dashboard if you have not seen it before.

Connection pools and queueing: Little's Law does the arithmetic

Little's Law says the average concurrency in a system equals throughput multiplied by average latency. That one line explains most pool exhaustion. A pool of 20 connections where each query holds a connection for 90 milliseconds sustains roughly 220 queries per second and not one more, no matter what the application servers are doing. Above that, requests wait for a connection, wait time counts as latency, and the whole thing tips over fast because slower queries hold connections longer, which starves the pool further.

The counter-intuitive part is that raising the pool size is often the wrong fix. Past the point where the database has enough cores to run the queries concurrently, more connections add contention inside the database and everything gets slower. The HikariCP documentation makes this argument well and the reasoning applies to any pool. Thread pools, HTTP client pools and worker queues all behave the same way. Measure the wait time inside the pool, not just its size.

Garbage collection and memory

On managed runtimes, look at allocation rate before pause times. A service allocating aggressively per request drives collection frequency, and the pauses are a symptom of the allocation, not an independent problem. On the JVM, G1 with a pause target is fine for most workloads, ZGC and Shenandoah exist for latency-sensitive ones, and switching collector before understanding the allocation profile is a common way to spend a week for nothing. In Go, the pauses are small but allocation still costs, and escape analysis in pprof tells you where. Node.js runs into a heap ceiling that has to be raised deliberately.

A soak test is the only reliable way to separate a leak from normal churn, because both look identical over twenty minutes and only one of them is still climbing after eight hours.

When the Database Is the Ceiling

In the majority of engagements we run, the binding constraint is in the data layer. The application tier scales horizontally by design and the database usually does not, so it is where load accumulates. Here is what we look for and in what order.

The query that was fine on ten thousand rows

Query plans change as tables grow, and the change is a cliff rather than a slope. An index the optimiser used at ten thousand rows gets abandoned at ten million in favour of a sequential scan, or a nested loop join that was cheap becomes ruinous once the outer relation grows. This is why testing against a small dataset produces misleading results: you are exercising a different execution plan from the one production runs. pg_stat_statements on PostgreSQL and the performance schema on MySQL rank queries by total time rather than by individual slowness, which is the ranking that matters, because a two millisecond query called four thousand times per request is a bigger problem than a two second report nobody runs.

EXPLAIN with ANALYZE and BUFFERS is where the argument gets settled. Rows estimated against rows returned exposes stale statistics. Buffer counts separate memory reads from disk reads, which is the number that tells you whether your working set fits.

N+1 access patterns, still the most common finding

An ORM lazily loading a relation inside a loop turns one page into four hundred round trips. Functional tests never catch it because four hundred fast queries against a warm local database still render in under a second. Under load, with a network hop and a contended pool, the same page takes eight seconds. Query logging during a single request is the cheapest possible diagnostic, and it is astonishing how rarely anyone has run it.

Lock waits and hot rows

Row-level lock contention appears wherever many transactions update the same record: an inventory count, a sequence table, a per-tenant counter, an aggregate updated on every write. PostgreSQL exposes waits through pg_locks and pg_stat_activity, InnoDB through its lock tables. Long transactions make everything worse because they hold their locks until commit, so an application that opens a transaction, calls an external API and then commits is holding database locks for the duration of somebody else's network latency.

Isolation level is worth checking too. Serialisable on PostgreSQL will abort transactions under concurrency, and if the application does not retry them properly you get errors under load that are entirely invisible in single-user testing.

Connections, pooling and the managed database ceiling

Every backend connection costs memory, and PostgreSQL is particularly sensitive because of its process-per-connection model. Managed instances ship with a max_connections value tied to instance size, and application autoscaling multiplies the problem: twenty pods with a pool of twenty each want four hundred connections from a database configured for two hundred. PgBouncer in transaction pooling mode is the usual answer, with the caveat that it breaks session-level features such as prepared statements and advisory locks unless configured for them. Serverless and Lambda-style architectures hit this harder still, since each concurrent invocation may want its own connection.

The maintenance that only shows up in a soak

Autovacuum on PostgreSQL, purge on InnoDB, index maintenance, statistics refreshes and checkpoints all run on their own schedule, and a heavy write test triggers them at a point a short run never reaches. A soak test that shows periodic latency spikes every forty minutes is usually showing you a checkpoint or a vacuum, which is useful to know before it happens during a sale rather than after.

CDN and Caching: Where Results Are Most Easily Faked

Caching is the reason a performance test can report a system handling ten times its real capacity. If the test requests a small set of cacheable URLs, the CDN answers almost everything from an edge node close to the load generator, the origin sees a trickle, and the numbers are excellent. You have measured Cloudflare or Fastly, who were never in doubt.

So the first question in any test design is which layer we are aiming at. Testing through the CDN measures the real user journey and tells you whether your cache configuration works. Testing against the origin directly, bypassing the edge, measures the capacity that actually matters when the cache is cold or the content is personalised. Both are legitimate. Reporting one and describing it as the other is not, and it happens often.

Cache key design is where most real problems live. Query parameters usually form part of the key, so campaign tracking parameters can shatter the hit rate: the same page requested with fifty different UTM combinations becomes fifty separate cache objects, each needing its own origin fetch. Normalising or ignoring tracking parameters is often the single highest-value change we recommend on a content-heavy site. A careless Vary header does the same damage, splitting the cache by user agent or by a cookie that changes per visitor. Cookies are the classic culprit, since one session cookie on a static asset response can make an entire path uncacheable.

Then there is the stampede. When a popular cached object expires, every concurrent request for it misses simultaneously and they all reach the origin at once. A page served comfortably from cache at two thousand requests per second becomes two thousand simultaneous origin requests the instant its TTL elapses. The defences are well understood: request coalescing at the edge, stale-while-revalidate so users get slightly old content while one request refreshes it, jittered TTLs so a thousand objects do not expire together, and origin shielding or tiered caching so only one edge location talks to your origin. A spike test aimed at a cold cache is the test that finds whether any of this is configured, and it is rarely run.

Application-level caches deserve the same suspicion. A Redis instance is a shared resource with its own single-threaded limits, and a load test can find its ceiling as easily as the database. Watch for the same stampede pattern behind it, for keys with no expiry accumulating memory across a soak run, and for the case where the cache lookup plus deserialisation costs more than the query it replaced. That last one is more common than anyone admits, particularly when someone has cached a small, well-indexed query result.

One practical note before you run anything at scale: cloud providers and CDN vendors generally have acceptable-use terms covering generated load, and testing above certain volumes may need notification. We check the specifics for your providers with you before a run rather than discovering the policy afterwards.

How Do You Put Performance Tests in CI Without Adding an Hour to Every Build?

You split the question. The full peak, soak and breakpoint sequence does not belong in a pull request pipeline and never will, because it needs a stable environment, a warm system and a long enough window to be meaningful. What belongs in the pull request is a much smaller test whose only job is to catch an obvious regression before it merges.

The pull request test: two to three minutes, hard thresholds

A short run at low, constant arrival rate against the critical endpoints, with declared thresholds on p95, p99 and error rate. Pass or fail, nothing to interpret. In k6 this is the thresholds block and a non-zero exit code, which is all a CI job needs. It will not find a subtle two percent degradation and it is not supposed to. It catches the change that added a database call inside a loop, and it catches it while the author still has the context to fix it, which is the whole point.

Keep the environment for this run as stable as you can manage. Shared CI runners are noisy neighbours, and a threshold that fails randomly gets disabled within a fortnight, at which point you have the maintenance cost of the test with none of the benefit. Where absolute numbers are too unstable to trust, compare against a baseline captured on the same infrastructure in the same run and assert on the delta instead.

The nightly run: the real measurement

Full peak load against a persistent environment, on a schedule, with results stored as a time series rather than as a pass or fail. This is where you see the drift that a single run cannot: p99 climbing four percent a week for two months is invisible day to day and obvious on a chart. Comparing every night against the last known good run turns performance into something with a trend line, which is what makes it manageable rather than dramatic.

Component benchmarks: the cheapest early warning

Not everything needs a deployed environment. JMH on the JVM, pytest-benchmark in Python, the built-in benchmark tooling in Go and Rust all measure a single function or handler in seconds, with statistical treatment of variance. They will not tell you about pool exhaustion or cache stampedes, and they catch algorithmic regressions early and cheaply. A benchmark that fails when a hot function gets three times slower is worth having at the unit level.

Making the pipeline result useful

Post the numbers into the pull request as a comment, comparing this run against the base branch. A developer who sees p95 went from 180 to 340 milliseconds on the endpoint they touched will investigate. A developer who has to open a separate dashboard and remember what normal looks like will not. This is a small piece of plumbing with a large effect on whether anyone acts. Wiring it in properly is part of the same work as the rest of your delivery pipeline, which we cover under CI/CD pipeline services in India if the pipeline itself needs attention first.

Three Situations We Get Called For

These are composites drawn from the shape of the work rather than accounts of particular clients, and they are the patterns that repeat.

Checkout is fine at 200 requests per second and dead at 260

A retailer whose seasonal peak is a known date. Everything is comfortable through testing, then a narrow band where latency goes from 300 milliseconds to twelve seconds across a forty request per second increase, with application CPU at thirty percent throughout. The cliff shape is the clue: gradual degradation means a resource filling up, a cliff means a queue.

The investigation runs pool metrics alongside the load. Connection acquisition wait time, flat at zero, starts climbing at exactly the point latency does. The pool is capped at 20, the average query holds a connection for 90 milliseconds, and Little's Law puts the ceiling around 220 queries per second. What makes it a cliff rather than a slope is the feedback loop: once requests start queueing for connections they hold their threads longer, so the application thread pool fills too, and the load balancer starts failing health checks on instances that are alive but not answering.

The fix is rarely just a bigger pool. It is usually a combination: cut the number of queries per request so each one holds a connection for less time, add PgBouncer so the database is not asked for four hundred connections, size the pool against the database's actual core count rather than optimistically, and set an acquisition timeout so a saturated pool returns a fast error instead of holding a thread hostage. Then retest and confirm the cliff moved rather than assuming it did.

The service that has to be restarted every night

A B2B platform where somebody added a 3am restart eighteen months ago and nobody questions it any more. Latency is fine in the morning and noticeably worse by late afternoon, which everyone has learned to live with.

A twelve hour soak at moderate load reproduces it on the first attempt, which is usually the case with this class of problem. Heap climbs steadily, old generation occupancy never returns to its starting level after a full collection, and garbage collection time as a share of wall clock creeps from two percent to nineteen. A heap dump at the eight hour mark points at a map used as a cache, populated per unique customer identifier, with no eviction and no size bound. It was written when the platform had forty customers.

The change is small: a bounded cache with an eviction policy and an expiry, plus a gauge on its size exported to your metrics so the next version of this problem is visible before it needs a cron job. The valuable part of this engagement is not the fix, it is that a soak run in CI now fails if heap does not return to baseline, which is the thing that stops it recurring.

The lift-and-shift that got slower on better hardware

A team moves a monolith from a colocated data centre to a cloud provider, keeping the application unchanged and moving the database to a managed service. The new machines are objectively faster. Page loads are up by a factor of three, and everyone is baffled.

A load test with query-level tracing gets there quickly. In the old setup the application and database were on the same rack, with sub-millisecond latency between them, so a request making 380 sequential ORM queries paid almost nothing per query. In the new setup they sit in different availability zones, three milliseconds apart, and those same 380 queries now cost over a second of pure waiting. Nothing is slow. Everything is far away.

The remedies are ordinary once the cause is named: eager-load the relations causing the N+1, batch what can be batched, cache the reference data being re-fetched per request, and place the tiers to minimise cross-zone hops without giving up the availability you moved for. It is worth adding that this failure mode is a strong argument for testing before a migration rather than after, since the number that changed was one nobody thought to measure. Migration planning of this kind sits with our cloud architecture services in India.

How the Engagement Runs

A first round typically takes four weeks to produce findings you can act on. The shape below is the common case, and it moves depending on what already exists.

Week one: model and access

Traffic analysis from your logs or analytics, agreement on the critical journeys, the workload model written down and reviewed by you, environment decision, tool decision, and access to your observability stack. We also check what is missing, because a load test against a system with no per-endpoint latency metrics and no database query statistics will find a number and not a cause. Sometimes the first week's most useful output is a short list of instrumentation to add.

Week two: build and calibrate

Scripts written and committed, dynamic tokens correlated, response validation on every step, test data prepared. Then generator calibration, a smoke run at low load, and a check that the numbers move sensibly when the load moves. Finding out that the client machine is the bottleneck belongs here, not in the report.

Week three: run the sequence

Baseline, peak load, stress past the peak with recovery timing, and the breakpoint ramp. System metrics captured across every run. First findings shared as they emerge rather than saved for a reveal, because if the bottleneck is obvious on day two you should be fixing it on day three.

Week four: soak, profile, report

The long soak run, usually overnight or across a weekend, plus profiling on whatever the earlier runs implicated. Then the written analysis: findings, evidence, prioritised recommendations with effort estimates, and a walkthrough with your engineers where they can argue with the conclusions. Retesting after fixes and the CI integration follow, either immediately or when the fixes land.

How Does a Performance Testing Team in India Work Across Your Timezone?

Here are the real numbers, because vague reassurance about overlap is how offshore engagements start badly. A standard Indian working day of 09:30 to 18:30 IST maps onto your day as follows.

For a UK team that is 04:00 to 13:00 in winter and 05:00 to 14:00 during British Summer Time, giving roughly four hours of live overlap in winter and five in summer against a normal London day. Comfortable, and no shift changes needed. For Sydney, a standard IST day runs 15:00 to midnight local, leaving about two hours of overlap at the start of your afternoon; moving the Indian day earlier to 07:00 to 16:00 IST takes that to roughly four and a half hours. Auckland is harder: a standard IST day gives almost nothing, and an early Indian shift of 06:00 to 15:00 IST buys about three and a half hours against a normal New Zealand afternoon.

The United States is where honesty matters most. A standard IST day ends at roughly 08:00 US Eastern, which means zero overlap with a nine-to-five in New York. Three hours with the East Coast requires the Indian day to run to about 22:30 IST. Four hours with the West Coast requires it to run past midnight IST. That is a night shift. It is possible and people do it, and you should know what it costs: night work is harder to hire for, harder to retain, and the engineer doing it lives on a different sleep schedule from everyone else around them. Anyone selling you round-the-clock cover as though it were free is either running a rotation they have not mentioned or is not going to deliver it. We agree the working window with you before the engagement starts and write it down.

Why load testing suits the gap better than most work

This service fits a distributed model unusually well, and the reason is structural rather than a sales point. Long runs need a quiet environment and nobody using it, so the natural time to run a twelve hour soak or a full breakpoint sequence is your night. That is our day. The test executes while your engineers are asleep and your staging environment is idle, and the analysis is waiting when you get in.

The deliverable is also unambiguous, which removes most of the interpretation risk that makes offshore work hard. p99 at 400 requests per second either crossed your threshold or it did not. There is no stylistic disagreement about a latency histogram. Findings arrive as written analysis with evidence attached rather than as something that needs a live conversation to be understood.

How the days actually run

Written first, consistently. A daily written update in your Slack or Teams channel before your morning: what ran overnight, what it showed, what is blocked, and what needs a decision from you. Live calls happen during the agreed overlap, usually two or three a week rather than daily, because a daily call eats the overlap you wanted for working sessions.

Anything needing your input goes out as a written proposal with options and a recommendation, not as an open question, since an open question costs a full day of round trip. Blockers are raised before the Indian day ends rather than discovered at the start of yours. Runs that carry real risk, such as anything against production, are scheduled inside the overlap so both sides are awake, and we say in advance which ones those are.

Access, ownership and security

Work happens in your repositories, your cloud accounts and your observability tooling under named individual accounts with scoped roles, so your audit log shows who did what. Test scripts are committed to your version control from day one rather than developed privately and handed over at the end. If the engagement stops tomorrow, everything built is already yours because that is where it was built.

Terms covering IP assignment, confidentiality, data handling and access revocation belong in the agreement signed before work starts, and we settle those with you up front rather than quoting a policy from a marketing page. If your security team has a questionnaire, send it before the engagement rather than during it. Where the test environment needs a copy of production data, the anonymisation approach and the legal basis are a conversation for your data protection people and your counsel, and we build against their answer.

The talent question for this specific skill

India has a very large pool of engineers who can record a JMeter script, and a much smaller one of engineers who can read a flame graph, reason about queueing, tell a garbage collection pause from a lock wait, and argue about whether a percentile was computed correctly. We staff against the second group for this work, and the vetting reflects it: candidates are given a system that degrades under load along with its metrics and asked to explain what is happening, rather than being asked to define load testing. Written English is assessed by having them write the analysis, since that is the artefact you will actually read.

What Goes Wrong, and How We Handle It

Most engagements of this kind hit some of the following. Naming them in advance is more useful than promising they will not happen.

There is no environment to test against

The most common blocker by a distance. Staging is a tenth of production, shared with three other teams, and someone is always deploying to it. We deal with this by agreeing early what the environment can and cannot support, which findings will be absolute and which only relative, and whether a temporary production-sized environment can be stood up for a window. What we do not do is run against an unsuitable environment and present the numbers as though they were production capacity.

The observability is not there yet

Without per-endpoint latency, database query statistics, pool metrics and runtime metrics, we can tell you that the system is slow and struggle to tell you why. Where the gaps are small we add instrumentation as part of the engagement. Where they are large it becomes a separate piece of work that has to come first, and we would rather say that in week one than produce a report full of hedged guesses.

Third parties you cannot generate load against

Payment gateways, identity providers, shipping rate services, tax calculators. Their sandboxes rate limit well below your test volume and pointing load at their production systems is not acceptable. We stub them with realistic latency distributions including a slow tail, and we test your timeout, retry and circuit breaker behaviour explicitly, since that is where third-party trouble actually reaches your users.

The bottleneck turns out to be architectural

Sometimes the honest finding is that no configuration change gets you where you want to be, because a single-writer database or a synchronous call chain sets a ceiling that tuning cannot lift. That is an uncomfortable report and it is the one worth paying for, delivered with options and rough effort rather than as a verdict. It is better than six weeks of tuning that buys twelve percent.

The findings land in a queue nobody owns

A report with fourteen recommendations and no assigned owner produces nothing. We push for named owners and a retest date at the walkthrough, and we prioritise ruthlessly so the top three are unambiguous. Where the fixes need engineers you do not currently have, hiring dedicated DevOps developers in India is one route and there are others, but the decision is better made explicitly than by letting a backlog absorb the work.

Numbers that will not sit still

Runs on shared infrastructure vary, sometimes by a lot. Noisy neighbours, background jobs, autovacuum, a colleague deploying mid-run. We handle it by repeating runs and reporting variance rather than a single figure, by recording what else was happening, and by preferring comparisons within a run over comparisons across days where the environment cannot be held still.

Continuity of the people doing the work

People change roles and companies, in India as everywhere. What we control is exposure: no single engineer is the only one who understands your test suite, the scripts and the analysis live in your repository from the start, and documentation is written by whoever did not build the thing being documented. How notice, handover and replacement work commercially is set out in the agreement before work begins, in writing, rather than left for you to assume.

Engagement Models

Performance assessment

A fixed-scope engagement producing the workload model, the test suite, the full run sequence and the bottleneck analysis with prioritised recommendations. Useful on its own and the usual starting point. You keep the scripts and the report regardless of what happens next.

Project engagement

A defined piece of work scoped after the assessment: implement the fixes, build the regression harness, prepare a system for a known peak event, or re-test through a migration. Scoped against measurements rather than against a conversation, because estimating this work before measuring produces fiction.

Dedicated engineers

One or more performance engineers working inside your team, your board and your review process on an ongoing basis. Suits organisations where load is a permanent concern with a queue of work rather than a one-off project. Composition and duration are agreed with you before starting.

Most engagements start as an assessment and become one of the other two, which is deliberate. We would rather scope a build against measurements than against a hypothesis. If you already know what needs doing and have the numbers behind it, we can start at the second model.

Where This Sits Alongside Our Other Work

Performance work rarely arrives alone. Once you know your ceiling, the ongoing question of what to promise users and how much headroom to keep belongs with site reliability engineering in India, which covers service level objectives, error budgets and capacity planning. We deliberately keep those separate: this page is about finding the limit, that engagement is about operating within it.

Where the constraint turns out to be how your workloads are scheduled and scaled, requests, limits, autoscaler behaviour and pod disruption budgets are cluster questions covered by our Kubernetes services in India. Where the short regression test needs to become part of the release process, that is CI/CD pipeline work. Broader test strategy and the layers of the test pyramid sit with our QA and testing services.

Buyers regularly ask for one kind of testing and mean another, so here is the boundary. This page is about behaviour under load: throughput, latency distributions and the component that gives out first. Whether the checkout flow works correctly at all is automated UI and end-to-end testing. Whether an endpoint returns the right payload and honours its contract is API testing, though the two overlap in practice, since a load script with no response validation is just a fast way of measuring error pages. Whether the system resists attack is security testing, and rate limiting is the one place that work and this work meet directly.

On the client side, battery drain, thermal throttling and behaviour on a poor mobile network belong to mobile app testing rather than here, even though slow APIs show up in both. And accessibility testing against WCAG criteria is a separate audit with its own standards, sharing only the observation that a page which takes nine seconds to become interactive is unusable for everyone.

If what you need is engineers embedded in your team rather than a scoped project, hiring dedicated engineers in India or the equivalent for your stack is the same capability on a different commercial shape. And if the system is being redesigned rather than tuned, the placement of services, data stores and network boundaries is decided under cloud architecture, where the performance characteristics are set long before anyone runs a test.

Frequently Asked Questions About Performance Testing in India

How long does a performance testing engagement take?

A first useful answer usually takes three to four weeks. Week one is traffic analysis and environment work, week two builds and calibrates the scripts, week three runs the load and soak tests and produces the bottleneck report. Fixing what we find is a separate conversation, because the fix might be a two line pool setting or a six month re-architecture and we will not know which until we have measured. Retesting after a fix is fast, typically a day.

Should we use k6 or JMeter?

k6 if your team writes JavaScript, your test plan belongs in git, and you want the test to fail a pipeline on a threshold. JMeter if you need a protocol nobody else supports, such as JMS, JDBC or LDAP, or if your team already maintains a large set of jmx plans. JMeter costs more memory per virtual user because it uses a thread per user, and its XML plans are painful to review in a pull request. Neither choice will find or hide a bottleneck the other would not.

Can you load test our production environment?

Sometimes, and it is the only way to remove environment parity as a variable, but it needs planning rather than courage. That means a low-traffic window, a kill switch on the load generator, tagged synthetic traffic that your analytics and billing exclude, test accounts rather than real ones, and third-party integrations pointed at sandboxes. Your cloud provider and CDN may also have an acceptable-use policy for generated load, so we check that with you before anything runs.

How much load should we test for?

Not a number someone invented in a meeting. We take your busiest hour from the last twelve months out of access logs or your analytics tool, work out the request rate per endpoint in that hour, and use that as the baseline. Target load is that peak multiplied by whatever growth or campaign factor you can actually justify. Then we run a breakpoint test to find where the system stops coping, because the distance between your peak and your ceiling is the real answer you are buying.

Why do you keep asking about p99 instead of average response time?

Because an average hides the users who are having a bad time. A service can average 200 milliseconds while one request in fifty takes eight seconds, and averages will never show you that. Percentiles also compound: if a page makes twenty backend calls and each has a p99 of one second, the arithmetic says roughly one page view in five contains at least one slow call. We report p50, p95, p99 and the maximum, plus error rate, because that set is hard to fool.

What do you do about payment gateways and other third-party APIs in a load test?

We never point load at a partner's production system. Sandboxes usually rate limit well below your test volume, so the honest options are a stub that mimics the real latency distribution including its slow tail, or a recorded proxy. What matters is modelling the timeout, retry and circuit breaker behaviour, because most third-party incidents hurt you through the way your code waits rather than through the partner being down.

Can a performance test run in CI without adding an hour to the build?

Yes, if you stop trying to run the full test there. On a pull request we run a two to three minute smoke test at low, constant arrival rate with hard thresholds on p95 and error rate, so an obvious regression fails the build. The full peak, soak and breakpoint runs go on a nightly or pre-release schedule against a stable environment, and the result is tracked as a trend rather than as a single pass or fail.

How does a performance testing team in India work with our timezone?

A standard 09:30 to 18:30 IST day gives a UK team roughly four to five hours of live overlap and a US East Coast team almost none, so we agree the working window with you in writing before anything starts. This particular service suits the gap better than most: long soak and peak runs need a quiet environment and nobody watching, and the Indian working day sits neatly inside your night. You read the report with your coffee instead of waiting for the test to finish.

Tell Us What Falls Over, and at What Number

Send the endpoint, the traffic level where it starts hurting, and what your monitoring shows at that moment. We will come back with what we would measure first, what we expect to find, and whether this is worth a full engagement or a two day look.

Start the Conversation