Chapter 14Lesson 03~165 minutes

REST and JSON API Performance Testing: Configuration, Design Patterns, and Trade-Offs

API test design is a choice about what state and metric population the result represents. A resource-level GET benchmark answers a different question from Create→Read→Update→Delete journey timing, and a shared fixture answers a different concurrency question from per-user isolated resources.

Resource vs transactionPer-user stateSetup vs seedCleanup designAssertion cost

Learning objectives

  • Choose resource-level or end-to-end transaction measurements from the decision being made.
  • Choose shared immutable data or per-user mutable resources from concurrency semantics.
  • Compare setup API calls with pre-seeded fixtures.
  • Choose per-resource idempotent delete or a run-scoped teardown sweep.
  • Balance JSON validation depth against injector CPU/allocation.
  • Keep JMeter, JVM, OS/network, SUT, CI/container, and plugin configuration layers distinct.

1. Mandatory path remains local/free

Every runnable comparison stays on http://127.0.0.1:8000, ≤2 threads, bounded duration. No public demo API, cloud load service, managed database, Kubernetes cluster, enterprise identity, paid CI, or third-party JMeter plugin is required.

2. Resource-level versus end-to-end transaction tests

Resource-level End-to-end transaction
Measures one operation population: GET, POST, PUT, DELETE. Measures a business journey such as create→read→update→delete.
Easier attribution to one endpoint/service path. Captures compounded state/correlation and total user journey.
Needs setup data outside timed population when state is required. Includes multiple operations; cannot attribute latency to one endpoint without child metrics.
Best for endpoint regression/capacity questions. Best for business workflow/SLO questions.

It is valid to keep both in one plan/report if labels are explicit, but never quote the transaction p95 as “GET latency.”

3. Shared versus per-user data

Shared immutable data is efficient for read-only GET scenarios where many users legitimately read the same public/catalog record. Per-user mutable data is safer for update/delete/write scenarios because each thread owns its object and correlation ID.

Sharing one mutable resource is justified only if production users truly contend on that same resource and the contention itself is the workload objective.

4. Setup API calls versus pre-seeded fixtures

Setup API call Pre-seeded fixture
Exercises real public setup contract and yields current IDs. Keeps setup traffic outside measured run.
Can add time/errors before workload starts. Requires deterministic provisioning/versioning.
Good when each user needs dynamic state. Good for stable read benchmarks.
Must be labeled/excluded from timed metric if not part of objective. Must be cleaned/reset between runs to preserve baseline.

A Setup Thread Group can create run data, but variables are thread-group local. If it produces IDs needed by another Thread Group, use an explicit safe handoff store/file/property only when the ownership model is clear. For beginner labs, keep per-user creation in the same thread when correlation is needed.

5. Idempotent per-item cleanup versus teardown sweep

Per-item DELETE immediately after use minimizes target leakage and attributes cleanup failures to a resource. A final run-scoped sweep is a safety net for interrupted/failing iterations. The best checkpoint uses both:

  1. normal flow deletes ${ITEM_ID};
  2. post-run cleanup targets only ${RUN_ID};
  3. verification proves zero remaining run-scoped resources.

Never use an unscoped destructive sweep in a shared environment.

6. Teardown script/controller trade-off

A tearDown Thread Group can run cleanup after ordinary Thread Groups, but thread-local variables from workload threads are not automatically available there. A run-scoped cleanup endpoint avoids needing to transfer every ITEM_ID and remains bounded to one synthetic run.

If the generator crashes or is killed, even tearDown logic may not run. Operational cleanup must therefore also be runnable independently using the stored RUN_ID.

7. Response-body validation depth versus generator cost

Validation depth Value Cost/risk
Status code only Transport contract. Misses fast business errors.
Status + 1–3 scalar JSON fields Strong core business invariant. Usually low/moderate parse cost.
Many JSON paths on every response Broad contract coverage. Repeated parsing/checking adds injector work.
Full large-body schema/script validation Deep correctness. Potentially high CPU/allocation; may belong in functional tests rather than high-load path.

Use the smallest assertions that prove the load test is exercising the intended success path. Keep fuller schema/property tests in a separate functional/API test layer unless they are essential to load validity.

8. JSONPath versus JMESPath

JMeter provides both JSON Extractor/Assertion (JsonPath) and JSON JMESPath Extractor/Assertion. This chapter uses JMESPath for simple scalar fields such as item.id, status, and item.version. Use one team-standard query language consistently where possible.

Current JSON assertions first parse the body and fail if it is not valid JSON; missing paths are also failures before optional expected-value comparison.

9. Header scope and authentication boundary

Content-Type: application/json belongs to JSON-body operations; Accept: application/json is broader. Authentication is intentionally absent from the Chapter 14 mandatory lab. Chapter 15 introduces authentication/tokens/session workflows.

If a real authorized API requires credentials, use a secure external secret mechanism and synthetic/non-production account; do not hard-code bearer/API keys into JMX or HTML lessons.

10. Retry policy is operation-semantic, not universal

GET may be naturally safe to retry from a resource-state perspective, while POST may create a duplicate if the first response is lost after server commit. Blindly retrying writes can change the workload and corrupt test data.

If production supports idempotency keys, model them explicitly in the authorized environment. This chapter does not invent a retry layer; it preserves first failure and cleans by run ID.

11. Result schema and dashboard

Keep stable labels such as Create Item, Read Item, Update Item, Delete Item, and Resource Lifecycle. CSV JTL should retain label, elapsed, success, response code/message, thread counts, and assertion failure messages. Generate dashboard after/at CLI run and inspect operation-level statistics/errors.

Do not save full successful response bodies in high-load CSV; current CSV output does not store response data by default. Sensitive response retention should be opt-in, bounded, and reviewed.

12. Configuration-layer boundaries

Layer Examples Not a substitute for
JMeter core plan HTTP samplers, defaults, headers, extractors, assertions, timers SUT business correctness or DB reset.
Java/JVM heap, GC, TLS/JDK Fixing hard-coded IDs or over-deep assertions without evidence.
OS/network sockets, DNS, loopback/firewall API resource isolation.
SUT HTTP methods/statuses, validation, datastore, service metrics JMeter variable scope.
Plugin/driver None required here Mandatory JSON/HTTP functionality already in core.
CI/container runner CPU, workspace, optional containerized fixture Correct RUN_ID cleanup/authorization.

13. Worked scenario

Decision: “We need a release gate for catalog GET latency and a separate sanity gate proving create/update/delete still works under low concurrency.”

  • Pre-seed 10 immutable synthetic catalog items → GET-only timed profile; random/partitioned reads; status + key JSON assertions.
  • Separate 1–2 thread lifecycle profile → each thread creates/updates/deletes its own item tagged with RUN_ID.
  • Do not mix write lifecycle samples into the GET latency population.
  • Require zero leaked RUN_ID resources before declaring the run valid.

14. Evidence contract

For every meaningful run preserve raw JTL + matching jmeter.log, exact JMX/properties/RUN_ID, fixture version/schema, event log/metrics, generator CPU/memory, and cleanup verification. Configured thread/duration settings without achieved sample counts are insufficient.

15. Decision table

Requirement Preferred design Reason
Endpoint GET regression Resource-level GET with pre-seeded/immutable data Clean metric population.
Mutable per-user workflow Create/extract/use/delete inside same thread Thread-local correlation and isolation.
Recover from failed iteration Per-item delete + run-scoped final sweep Immediate + fallback cleanup.
Deep schema correctness Separate functional/schema layer unless essential under load Avoid unnecessary injector cost.
Read/write mix Explicit operation proportions/labels Maintain valid causal interpretation.

Knowledge check

When is shared data appropriate?

Why can a tearDown Thread Group not simply use each workload thread's ITEM_ID?

Why use a run-scoped cleanup endpoint as a fallback?

Why isn't full JSON schema validation always ideal in a hot load path?

What makes configured load different from achieved load?

Next lesson

Diagnose API failures without corrupting state

Lesson 4 deliberately creates shared-resource conflicts, stale IDs, business-error blindness, cleanup mistakes, sensitive-body retention, and blind write retries.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against current Apache JMeter primary documentation on 2026-09-05. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK for labs and no third-party plugins; JMeter 5.6.3 requires Java 8+. HTTP Request samplers are used directly for GET/POST/PUT/DELETE. JSON JMESPath Extractor is a built-in Post-Processor for extracting response values into JMeter variables; JSON/JSON JMESPath Assertions parse JSON and fail on missing/invalid paths before optional value comparison. Assertion failure messages are enabled by default in CSV result output and are kept explicit in chapter commands. Meaningful load runs use CLI mode with raw CSV JTL plus matching jmeter.log; HTML dashboard generation uses the same JTL. The mandatory API fixture is Python-standard-library only and runs on loopback, so no container, cloud service, database, external API, driver, or plugin is required.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.