Forum Diskusi dan Komunitas Online

Full Version: Performance Testing for Microservices: Challenges, Strategies, and Solutions
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
[attachment=9674]

Performance Testing for Microservices: Challenges, Strategies, and Solutions
Every service on the dashboard was green. The customer's checkout page was still spinning.

If you've run a microservices estate for more than a year, you've probably lived some version of that sentence. Each team can show you a passing performance test for its own service. Each service meets its latency target. And yet the thing a customer actually does, which crosses eight or nine of those services in a row, is slow on Monday mornings and occasionally falls over.

Nobody lied. The tests were honest. They just measured the wrong unit. A monolith has one place to look. A microservices system has a graph, and performance lives in the edges between the nodes, not only in the nodes themselves.

So this isn't a tour of tools. It's a way of deciding what to test at which level, why the usual approach leaves gaps, and what to do about the gaps that remain.
[attachment=9675]

Why the Monolith Playbook Breaks
In a monolith, a function call costs nanoseconds and never fails halfway. In a microservices system, the same call becomes a network hop. It can be slow, it can time out, it can be retried, and it can succeed after the caller has already given up. Four things follow, and they're the root of most of the pain.

Latency adds up, and tails add up faster. A request that touches several services inherits the slowest of them. Here's a small calculation with idealized numbers. If a page fans out to 10 services in parallel and each has a 1 percent chance of a slow response, the chance that at least one is slow is 1 minus 0.99 to the tenth power, about 9.6 percent. A "p99 problem" in each service becomes a roughly one-in-ten problem for the page. Real calls aren't perfectly independent, so treat that as a direction and not a forecast, but the direction is reliable.

Retries multiply load. Suppose each of three services in a chain makes up to three attempts when a call fails. In the worst case a single user request can produce 3 × 3 × 3 = 27 attempts against the deepest service. Everyone's retry policy is reasonable on its own page of configuration. Together they're an amplifier that switches on exactly when the system is struggling.

Failure spreads sideways. One slow dependency holds threads and connections in every caller. The callers slow down, their callers back up, and a problem in a small service becomes everyone's problem. Engineers sometimes call this a cascading failure, and it rarely shows up in a test of any single service.

Ownership is fragmented. Nobody owns the journey. Each team owns a slice, and the budget for the whole thing belongs to a product manager who can't see inside any of them.

A Ladder, Not a Single Test
Climb it from the bottom. Cheap rungs catch most problems early, and the expensive rungs are reserved for questions only they can answer. Teams that skip straight to the top rung end up with slow, flaky, expensive tests that nobody trusts, because every failure could be anyone's fault.

Rung one and two: keep each service honest
Test each service against a budget that was derived from the journey, not invented locally. If checkout has a two second target and passes through six services, the product owner and the teams need a conversation about who gets what. One caution: percentile budgets don't add. You can't give six services a 300 millisecond p95 each and promise 1,800 milliseconds at the end. Use budgets to guide design, then verify the real end-to-end percentile with a journey test.

The most important choice here is how you stub dependencies. A stub that answers instantly with a perfect response is the most common source of flattering results I see. Make stubs slow in realistic ways: a latency distribution with a tail, occasional errors, and the occasional throttle. That small effort makes the service's own test far more honest.

Rung three: test the neighbors, not just the service
This is the rung most teams skip, and it's where cascades get caught. You're not testing whether the service is fast. You're testing how it behaves when someone else isn't.

If you run a service mesh, fault injection is cheap. Here's a short Istio example that adds a three second delay to half of the calls to a pricing service, so you can watch what the callers do. The names and numbers are placeholders.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: pricing-slow-experiment
spec:
  hosts: [pricing]
  http:
  - fault:
      delay:
        percentage: { value: 50 }
        fixedDelay: 3s
    route:
    - destination: { host: pricing }


Then ask the questions that matter. Does the caller time out sooner than three seconds, or hold a thread for the whole wait? Does a circuit breaker open, and does it close again once things recover? Do retries pile on? Does the page degrade gracefully, perhaps by hiding recommendations, or does everything fail together? Run it only in an environment you're allowed to break, and have a one-step way to remove the fault.

Rung four: follow one journey
A journey slice is a deliberately small production-like environment containing only the services one business flow touches, with traces turned on. You aren't trying to simulate the company. You're trying to answer "does placing an order stay within budget, and where does the time go?"

Distributed tracing is what makes this rung worthwhile. With a trace, each slow request shows a timeline across services, so you can see that 40 percent of the delay sits in one downstream call. Without it, you have a graph that says the journey is slow and a meeting about whose fault that is.

Rung five: rehearse the whole estate
The full-system test is expensive, so use it sparingly and on purpose: before a launch, a seasonal peak, a migration, or a big architectural change. It should include realistic data volumes, realistic traffic mixes, production-like autoscaling rules, the real behavior of shared infrastructure such as message brokers, databases and gateways, and the failure of at least one component partway through. This is also where spike behavior, scale-out lag and recovery get examined, since those only appear when everything is connected.

The Challenges That Don't Go Away
Even with the ladder in place, several problems stay hard. I'd rather name them than pretend a tool solves them.

Environments. A faithful copy of a hundred-service estate is costly. Most organizations test with a reduced one, and the honest approach is to record how it differs from production and what that means for the confidence you can claim. Smaller databases, fewer replicas and missing caches all change the results.

Test data. Services share data indirectly. A journey needs customers, orders and inventory that agree with each other across several databases. Plan data creation as seriously as scripting, and refresh it, since tests that reuse the same records hit caches that production traffic wouldn't.

Noisy shared infrastructure. On a shared cluster, another team's batch job can contaminate your numbers. Run tests in isolated or ephemeral namespaces when you can, and record neighboring load when you can't.

Asynchronous work. Many flows continue after the response, through queues and event consumers. A fast response with a growing backlog is not a pass. Measure consumer lag, time to completion and end-to-end delivery, alongside request latency.

Version and rollout drift. Different services deploy at different times. A performance regression can come from a dependency's release that your team never touched, so keep a baseline per journey and rerun it when neighbors change.

Observability cost. Full tracing of every request is expensive at scale. Sample sensibly, and keep complete traces for errors and slow requests.

What to Measure
For each service, watch rate, errors and duration, plus saturation of the resources it depends on: thread pools, connection pools, queues and CPU throttling. Container CPU limits deserve a special mention. A service can look idle on average while being throttled in bursts, which shows up in the tail and nowhere else.

For each journey, track the end-to-end percentiles, the error and timeout rate, the share of time spent in each service, and how many calls each user request generates. That last number, sometimes called the fan-out or amplification factor, tends to creep upward as teams add features, and a quietly growing call count is a leading indicator of future trouble.

Here's a small script that turns a trace export into that number. It's a sketch, and the field names would match your tracing backend.

from collections import defaultdict

def calls_per_request(spans):
    """spans: iterable of dicts with 'trace_id' and 'service'."""
    per_trace = defaultdict(int)
    for s in spans:
        per_trace[s["trace_id"]] += 1
    counts = sorted(per_trace.values())
    n = len(counts)
    return {"median": counts[n // 2], "p95": counts[int(n * 0.95) - 1]}


Chart that per release. If the median goes from 14 calls to 22 over a quarter, you've found a performance regression that no latency test would have flagged yet.

When You Shouldn't Do Much of This
Not every microservices system needs the full ladder. A handful of internal services with low traffic and a tolerant business doesn't justify a rehearsal environment. Rungs one and two, plus decent monitoring, may be plenty. Likewise, don't run fault injection in an environment shared with teams who didn't agree to it, and don't hold a release hostage to a flaky journey test that people have learned to ignore. A test nobody trusts is worse than no test, because it teaches the organization to shrug at red.

If Some Services Call Models
When one or more services call an LLM, a vector store or an agent framework, the graph gains nodes with unusual behavior. Model calls can take seconds and vary a lot with prompt and answer length. Providers enforce request and token limits, so capacity may be set by a contract and not by your servers. Agents may make several downstream calls per request, which raises the amplification factor sharply, and a loop that retries a failing tool call can generate machine-speed bursts.

Test these services with realistic prompt and response sizes, stub the provider with its real throttling behavior, and track time to first token and cost per request. AI can help with the surrounding work, such as summarizing traces, clustering slow requests or generating varied test prompts. But the verdict should come from budgets your teams agreed on, not from a model's reading of a dashboard.

Mistakes I See Repeated
Testing each service well and never testing the journey. Stubbing dependencies with instant answers. Setting retries at every layer without anyone owning the total. Running one large test at the end of a release and calling that a strategy. Measuring averages. Ignoring queues and consumers. And leaving observability until after the first incident, when it's hardest to add.

A Closing Thought
Pick your single most valuable journey and ask a plain question: how many service calls does one user request generate today, and what was it six months ago? If nobody can answer, you've found the first thing to build, because everything else in this article depends on being able to see the graph.

My expectation is that this area will move from scheduled performance tests toward continuous verification, where journey budgets and call-count baselines are checked on every deployment and fault experiments run as routine. The teams that get there first won't be the ones with the best load tool. They'll be the ones who decided early that performance belongs to the journey, not the service. If you'd like help building that, our team at PrimeQA Solutions works with engineering and product leaders on Microservices Performance Testing, from the first ladder rung to full-system rehearsals.