Performance Test an API with JMeter

Build a correlated JMeter test suite with CSV data, assertions, and HTML reports.

Introduction

30 Second Summary

Every app has a breaking point. When thousands of users hit your API at once, you need to know whether it will hold up or collapse. Performance testing is how teams find that answer before their customers do.

In this project, you will build a production-quality performance test suite using Apache JMeter that tests a real API with correlated request chains, data-driven parameterization, automated assertions, and professional HTML reporting.

What You'll Build

A complete performance test suite that tests the Swagger Petstore v3 API using real-world load testing techniques including correlated workflows, CSV-driven test data, and CLI-mode execution with dashboard reporting.

By the end of this project, you'll have:

  • A correlated test plan (.jmx) with chained API requests that pass dynamic data between samplers using JSON Extractor.
  • A CSV data file for parameterized pet creation that simulates realistic, varied user traffic.
  • A generated HTML Dashboard Report showing response time percentiles, throughput graphs, and error breakdowns from a CLI-mode load test.
  • Secret Mission: Configure a spike test that simulates a sudden burst of 50 concurrent users and compare the results against your steady-state baseline.

Are there any prerequisites?

This project assumes you have basic familiarity with JMeter concepts like Thread Groups, Samplers, and Listeners. You should have JMeter 5.6.3 installed with Java 8+ (Java 17+ recommended).

Set Up the Test Plan Structure

Every performance test suite starts with a solid foundation. Before you can simulate user workflows or measure response times, you need a test plan that knows where to send requests and how many virtual users to create.

In this step, you will create the skeleton of your JMeter test plan. You will configure a Thread Group to define your virtual users, set up shared defaults so every request targets the Petstore API, and verify connectivity before building anything complex.

In this step, get ready to:
  • Create a test plan with a Thread Group that simulates 5 virtual users.
  • Configure shared HTTP defaults and headers for all requests.
  • Validate your connection to the Petstore API with a health check request.
Create the test plan skeleton

A Thread Group is JMeter's way of defining virtual users. Each thread represents one simulated user executing your test scenario.

  • Open JMeter by navigating to your JMeter installation's bin directory in a terminal.
  • Launch the GUI by running this command:

macOS

./jmeter.sh

Windows

jmeter.bat

Linux

./jmeter.sh

The JMeter GUI opens with a default Test Plan in the left panel.

  • Click on Test Plan in the left panel to select it.
  • In the Name field on the right, change the name to Petstore Performance Suite.
  • Right-click Petstore Performance Suite in the left panel.
  • Select Add, then Threads (Users), then Thread Group.
  • In the Name field for the Thread Group, enter Pet Lifecycle.
  • Set Number of Threads (users) to 5.
  • Set Ramp-Up Period (seconds) to 10.
  • Set Loop Count to 2.

What do these Thread Group settings mean?

Number of Threads is the count of virtual users JMeter simulates. Ramp-Up Period is how long JMeter takes to start all threads. With 5 threads and a 10-second ramp-up, JMeter starts one new user every 2 seconds.

Loop Count tells each thread how many times to repeat the test scenario. A loop count of 2 means each virtual user runs through the full workflow twice.

Now you need an HTTP Request Defaults element. This stores the base URL so you do not have to repeat it in every single request sampler.

  • Right-click Petstore Performance Suite (the Test Plan, not the Thread Group) in the left panel.
  • Select Add, then Config Element, then HTTP Request Defaults.
  • In the Protocol [http] field, enter https.
  • In the Server Name or IP field, enter petstore3.swagger.io.
  • In the Path field, enter /api/v3.

Why add HTTP Request Defaults at the Test Plan level?

Placing this element under the Test Plan (not the Thread Group) means every sampler in the entire plan inherits the base URL. Each individual request only needs to specify its unique path (like /pet or /store/inventory). If the API's base URL changes, you update it in one place.

Add the HTTP Header Manager

The Petstore API expects JSON request bodies and returns JSON responses. An HTTP Header Manager tells the API what format you are sending and what format you want back.

  • Right-click the Pet Lifecycle Thread Group in the left panel.
  • Select Add, then Config Element, then HTTP Header Manager.
  • Click the Add button at the bottom of the HTTP Header Manager panel.
  • In the Name column, enter Content-Type.
  • In the Value column, enter application/json.
  • Click Add again to create a second row.
  • In the Name column, enter accept.
  • In the Value column, enter application/json.

What do these headers do?

The Content-Type header tells the API that your request body is JSON. The accept header tells the API you want JSON responses back.

Placing the Header Manager under the Thread Group means every request sampler in that group automatically includes these headers. You will not need to set them individually on each request.

Validate connectivity with a health check

Before building complex workflows, you should confirm that JMeter can reach the Petstore API and get a valid response. A quick health check saves you from debugging connection issues later.

  • Right-click the Pet Lifecycle Thread Group.
  • Select Add, then Sampler, then HTTP Request.
  • Set the Name to Health Check - Get Inventory.
  • Set Method to GET.
  • Set Path to /store/inventory.

Why only the path and not the full URL?

Because you set https, petstore3.swagger.io, and /api/v3 in the HTTP Request Defaults, JMeter automatically prepends those to whatever path you enter here. The full URL becomes https://petstore3.swagger.io/api/v3/store/inventory.

Now add a listener so you can see the response.

  • Right-click the Health Check - Get Inventory sampler.
  • Select Add, then Listener, then View Results Tree.
  • Press Cmd+R (macOS) or Ctrl+R (Windows) to run the test.
  • Click on View Results Tree in the left panel.
  • Click the Health Check - Get Inventory result in the listener.

You should see a green icon next to the request (indicating success) and a 200 response code. Click the Response data tab to see JSON data showing pet inventory counts by status.

Seeing a red icon or connection error?

Check that your HTTP Request Defaults has https in the Protocol field (not http). The Petstore API requires HTTPS.

Verify that the Server Name is exactly petstore3.swagger.io with no trailing slash. Also confirm the Path in HTTP Request Defaults is /api/v3 (not /api/v3/ with a trailing slash).

If you see a timeout, check your internet connection and firewall settings. Help me debug my JMeter connection to the Petstore API

Now that you have confirmed connectivity, you should disable the View Results Tree listener. Listeners consume memory and CPU during load tests, which skews your results.

  • Right-click View Results Tree in the left panel.
  • Select Disable.

Why disable the listener?

JMeter's GUI listeners (like View Results Tree) store every response in memory. During a real load test with hundreds of requests, this eats RAM and slows down JMeter itself.

The official JMeter documentation explicitly warns: GUI mode should only be used for creating the test script. CLI mode must be used for load testing.

  • Save your test plan by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Save it as petstore_performance.jmx in your JMeter bin directory.

Your test plan skeleton is ready. You have a Thread Group simulating 5 users, shared base URL defaults, and confirmed connectivity to the Petstore API. Next up, you will build the correlated API workflow that chains requests together using dynamic data.

Build the Correlated API Workflow

Your test plan structure is ready. Now comes the technique that separates basic load tests from production-quality ones: correlation.

In real applications, API calls depend on each other. You create a resource, then use its ID to read, update, or delete it. A realistic performance test must capture that dynamic ID from one response and pass it into the next request. This is called a correlated workflow.

In this step, get ready to:
  • Create a POST request that adds a new pet to the Petstore API.
  • Extract the pet's dynamic ID from the response using a JSON Extractor.
  • Chain GET, PUT, and DELETE requests that use the extracted ID to complete the full pet lifecycle.
Create a pet with a POST request

The first request in your workflow creates a pet. The Petstore API returns the created pet object with a dynamically generated ID. That ID is what you will extract and feed into subsequent requests.

  • Right-click on the Pet Lifecycle Thread Group in the left panel.
  • Select Add > Sampler > HTTP Request.
  • Name this sampler Create Pet.
  • Set the Method dropdown to POST.
  • Set the Path field to /pet.
  • Click the Body Data tab and paste the following JSON:
{
  "name": "TestPet_${__threadNum}",
  "category": {"id": 1, "name": "Dogs"},
  "photoUrls": ["https://example.com/photo.jpg"],
  "tags": [{"id": 1, "name": "load-test"}],
  "status": "available"
}

What does this JSON body do?

This creates a pet with a unique name per thread. The ${__threadNum} function is a built-in JMeter variable that returns the current thread number (1, 2, 3, etc.).

The name and photoUrls fields are required by the Petstore API schema. The tags field marks this pet as part of your load test, making it easy to identify test data.

Now add an assertion to automatically verify this request succeeds.

  • Right-click on the Create Pet sampler in the left panel.
  • Select Add > Assertions > Response Assertion.
  • In the Field to Test section, check Response Code.
  • Set Pattern Matching Rules to Equals.
  • Click Add at the bottom of the Patterns to Test section and type 200.

What is a Response Assertion?

A Response Assertion automatically checks each response against your expected conditions. If the response code is not 200, JMeter marks that sample as a failure in the results.

Extract the pet ID with JSON Extractor

When the Petstore API creates a pet, it returns the full pet object including a dynamically generated id field. You need to capture that ID so your next requests can reference the exact pet you just created.

  • Right-click on the Create Pet sampler.
  • Select Add > Post Processors > JSON Extractor.
  • Set Names of created variables to petId.
  • Set JSON Path expressions to $.id.
  • Set Match No. to 1.
  • Set Default Values to NOT_FOUND.

What does the JSON Extractor do?

The JSON Extractor is a Post Processor that runs after the sampler completes. It parses the JSON response using a JSONPath expression and stores the result in a JMeter variable.

The expression $.id means "grab the top-level id field from the response." That value gets stored in ${petId}, which you can reference in any subsequent sampler.

The default value NOT_FOUND acts as a safety net. If extraction fails, subsequent requests will use NOT_FOUND instead of a blank value, making failures easy to spot in your results.

  • Save your test plan with Ctrl+S.

Your left panel should now show the JSON Extractor nested under the Create Pet sampler, along with the Response Assertion.

Don't see the JSON Extractor under Create Pet?

Make sure you right-clicked on the Create Pet sampler specifically, not on the Thread Group. Post Processors must be children of the sampler they extract data from.

If the extractor ended up at the wrong level, drag and drop it in the tree so it appears indented under Create Pet.

Help me fix my JSON Extractor placement in JMeter.

Chain GET, UPDATE, and DELETE requests using the extracted ID

With the ${petId} variable captured, you can now build the rest of the pet lifecycle. Each request will reference that extracted ID to operate on the exact pet your thread created.

Start with the GET request to retrieve the pet you just created.

  • Right-click on the Pet Lifecycle Thread Group.
  • Select Add > Sampler > HTTP Request.
  • Name this sampler Get Pet by ID.
  • Set Method to GET.
  • Set Path to /pet/${petId}.

Now add a JSON Assertion to verify the response contains the correct pet.

  • Right-click on the Get Pet by ID sampler.
  • Select Add > Assertions > JSON Assertion.
  • Set Assert JSON Path exists to $.name.
  • Check the Additionally assert value checkbox.
  • Set Expected Value to TestPet_${__threadNum}.

What is a JSON Assertion?

A JSON Assertion checks the content of a JSON response body, not just the status code. Here it confirms that the pet returned by the GET request has the same name you used when creating it.

This catches correlation bugs. If the ${petId} extraction failed, this assertion would fail because the response would not match the expected pet name.

Next, add the PUT request to update the pet's status from "available" to "sold."

  • Right-click on the Pet Lifecycle Thread Group.
  • Select Add > Sampler > HTTP Request.
  • Name this sampler Update Pet Status.
  • Set Method to PUT.
  • Set Path to /pet.
  • Click the Body Data tab and paste the following JSON:
{
  "id": ${petId},
  "name": "TestPet_${__threadNum}",
  "category": {"id": 1, "name": "Dogs"},
  "photoUrls": ["https://example.com/photo.jpg"],
  "tags": [{"id": 1, "name": "load-test"}],
  "status": "sold"
}

Why does the PUT request need the full object?

The Petstore API's PUT endpoint replaces the entire pet object. You must send all fields, not just the one you want to change. The key difference from your Create Pet body is the id field (using your extracted ${petId}) and the status changed to sold.

Finally, add the DELETE request to clean up the pet at the end of the lifecycle.

  • Right-click on the Pet Lifecycle Thread Group.
  • Select Add > Sampler > HTTP Request.
  • Name this sampler Delete Pet.
  • Set Method to DELETE.
  • Set Path to /pet/${petId}.
  • Save your test plan with Ctrl+S.

Your left panel should now show four HTTP Request samplers under Pet Lifecycle, in this order: Create Pet, Get Pet by ID, Update Pet Status, Delete Pet.

Verify the sampler order

JMeter executes samplers top-to-bottom. If your samplers are in the wrong order, drag and drop them in the left panel to rearrange. The Delete must come last, or the GET and PUT will fail with 404 errors.

Temporarily add a View Results Tree listener to run a quick validation. Right-click the Thread Group, select Add > Listener > View Results Tree. Run the test with Ctrl+R and check that all four requests show green (success).

Once confirmed, disable the View Results Tree listener by right-clicking it and selecting Disable. Leaving it active during load tests skews your results.

Seeing 404 errors on Get Pet by ID or Delete Pet?

Check the Create Pet response in View Results Tree. Confirm it returns a JSON body with an id field. If the response is empty or an error, fix the Create Pet request first.

Verify your JSON Extractor has $.id in the JSON Path expressions field (not $id or id). The dollar sign and dot are required JSONPath syntax.

The Petstore demo server is shared publicly. Occasional 404 errors can happen if another user deletes the pet between your requests. If you see sporadic failures but most pass, that is expected behavior on a shared server.

Help me debug my JMeter correlated workflow.

Your correlated workflow is complete. Each request depends on real data from the previous one, just like a real user session. Next up, you will make this test plan even more realistic by feeding it varied data from a CSV file.

Add CSV Data-Driven Parameterization

Your correlated workflow is working. Every thread creates a pet, retrieves it, updates it, and deletes it in sequence. But there is a problem: every single thread creates the exact same pet data.

Real users do not all submit identical requests. In this step, you will create a CSV file with varied pet data and wire it into your test plan so each thread iteration uses different names, categories, and statuses.

In this step, get ready to:
  • Create a CSV data file with 10 rows of varied pet information.
  • Configure a CSV Data Set Config element to feed the data into your test plan.
  • Update your Create Pet request and assertions to use the CSV-driven variables.
Create the CSV data file

A CSV Data Set Config in JMeter reads one row of data per thread iteration. Each column becomes a variable you can reference anywhere in your test plan.

  • Navigate to your JMeter bin directory (or the directory where your petstore_performance.jmx file is saved).
  • Create a new file named pet_data.csv and paste the following content into it:
petName,category,status
Buddy,Dogs,available
Whiskers,Cats,pending
Goldie,Fish,available
Rex,Dogs,sold
Mittens,Cats,available
Nemo,Fish,pending
Max,Dogs,available
Luna,Cats,available
Charlie,Dogs,pending
Bella,Cats,available

What does this data do?

The first row is the header. JMeter uses it to map columns to variable names (${petName}, ${category}, ${status}).

The 10 data rows cover three categories (Dogs, Cats, Fish) and all three valid pet statuses (available, pending, sold). This variety makes your load test much more realistic than sending the same request body every time.

  • Save the file and confirm it appears in the same directory as your .jmx file.

CSV file not being read later?

Make sure the file is saved with the exact name pet_data.csv (no extra .txt extension). On Windows, File Explorer may hide extensions by default. Confirm the file is in the same directory as your petstore_performance.jmx file.

help me troubleshoot why JMeter is not reading my CSV file

Configure CSV Data Set Config

Now you need to tell JMeter how to read your CSV file. The CSV Data Set Config element connects the file to your Thread Group so each iteration picks up the next row of data.

  • In JMeter, right-click on the Pet Lifecycle Thread Group.
  • Select Add, then Config Element, then CSV Data Set Config.
  • Set Filename to pet_data.csv.
  • Set Variable Names to petName,category,status.
  • Set Delimiter to ,.
  • Set Recycle on EOF? to True.
  • Set Stop thread on EOF? to False.
  • Set Sharing mode to All threads.

What do these settings mean?

  • Recycle on EOF = True means JMeter goes back to the first row when it reaches the end of the file. Your 5 threads with 2 loops will need 10 rows total, so recycling ensures no thread runs out of data.
  • Stop thread on EOF = False prevents threads from stopping if they somehow exhaust the file. Combined with recycling, this guarantees continuous execution.
  • Sharing mode = All threads means all threads share the same file pointer. Each thread gets the next available row, so no two threads use the same row at the same time.
  • Save your test plan with Ctrl+S (Windows) or Cmd+S (macOS).
  • Confirm the CSV Data Set Config element appears under your Pet Lifecycle Thread Group in the left panel.

Don't see CSV Data Set Config in the menu?

Make sure you are right-clicking on the Thread Group itself, not on one of the HTTP Request samplers inside it. Config Elements are only available at the Thread Group or Test Plan level.

help me find the CSV Data Set Config option in JMeter

Update the Create Pet request to use CSV variables

With the CSV Data Set Config in place, JMeter now populates ${petName}, ${category}, and ${status} with fresh values on each iteration. You need to update your Create Pet request to use these variables instead of the hardcoded values.

  • Click on the Create Pet sampler in the left panel.
  • Select the Body Data tab.
  • Find the existing JSON body. It currently looks like this:
{
  "name": "TestPet_${__threadNum}",
  "category": {"id": 1, "name": "Dogs"},
  "photoUrls": ["https://example.com/photo.jpg"],
  "tags": [{"id": 1, "name": "load-test"}],
  "status": "available"
}
  • Replace the entire body with this updated version that uses your CSV variables:
{
  "name": "${petName}_${__threadNum}_${__iterationNum}",
  "category": {"id": 1, "name": "${category}"},
  "photoUrls": ["https://example.com/photo.jpg"],
  "tags": [{"id": 1, "name": "load-test"}],
  "status": "${status}"
}

What changed in this body?

  • ${petName}_${__threadNum}_${__iterationNum} combines the CSV pet name with the thread number and iteration number. This guarantees every single request creates a uniquely named pet, even across concurrent threads.
  • ${category} pulls the category from the CSV row (Dogs, Cats, or Fish).
  • ${status} pulls the status from the CSV row (available, pending, or sold). This tests the API's handling of all valid enum values.

Now you need to update the assertion on your Get Pet by ID request. The pet name now includes dynamic components that are harder to predict, so you will assert on the status field instead.

  • Click on the Get Pet by ID sampler in the left panel.
  • Expand it and click on the JSON Assertion child element.
  • Change the Assert JSON Path exists field from $.name to $.status.
  • Change the Expected Value from TestPet_${__threadNum} to ${status}.

Why assert on status instead of name?

The pet name now includes ${__iterationNum} which increments with each loop. Asserting on $.status confirms the API stored the pet correctly using a field that comes directly from your CSV data.

  • Save your test plan with Ctrl+S (Windows) or Cmd+S (macOS).
  • Run the test with Ctrl+R (Windows) or Cmd+R (macOS). If you still have a View Results Tree listener enabled, check the request bodies to confirm different pet names and categories appear across threads.

Seeing ${petName} literally in the request body?

This means JMeter is not resolving the variable. Check that your CSV Data Set Config has the Variable Names field set to exactly petName,category,status with no spaces around the commas. Also confirm the CSV file is in the correct directory relative to your .jmx file.

help me debug why JMeter is not resolving my CSV variables

Your test plan now generates varied, realistic traffic. Each thread iteration creates a different pet with a unique combination of name, category, and status. Next up, you will add timing controls and additional assertions to simulate real user behavior and enforce performance SLAs.

Add Assertions and Timers for Realism

Your test plan now sends realistic, varied data to the Petstore API using CSV parameterization. The next challenge is making your tests self-evaluating.

Right now, you'd have to manually check every response to know if something went wrong. Assertions automate that checking for you, flagging failures instantly. You'll also add timers to pace your requests like a real user would, instead of hammering the server at full speed.

In this step, get ready to:
  • Add a Duration Assertion to enforce a 3-second SLA across all requests.
  • Add a Gaussian Random Timer to simulate realistic think time between requests.
  • Add a Response Assertion to verify the Delete request completes successfully.
Add a Duration Assertion as an SLA check

In real performance testing, you usually have a Service Level Agreement (SLA) that defines the maximum acceptable response time. A Duration Assertion flags any request that exceeds a time threshold as a failure.

Adding this assertion at the Thread Group level means it applies to every sampler underneath it. You won't need to add it to each request individually.

  • Right-click on the Pet Lifecycle Thread Group in the test plan tree.
  • Select Add, then Assertions, then Duration Assertion.
  • In the Duration Assertion panel, set Duration in milliseconds to 3000.

What does this do?

Any request that takes longer than 3 seconds (3000 milliseconds) will be marked as a failure in your test results. This simulates a real SLA requirement where slow responses are just as bad as errors.

Because the Duration Assertion sits at the Thread Group level, it automatically applies to every sampler: Create Pet, Get Pet by ID, Update Pet Status, and Delete Pet.

  • Confirm the Duration Assertion appears as a direct child of the Pet Lifecycle Thread Group in the test plan tree (not nested under any sampler).

Don't see Duration Assertion in the menu?

Make sure you right-clicked on the Thread Group itself, not on an individual sampler. The Assertions submenu is available at both levels, but placing it at the Thread Group level is what makes it apply to all requests.

Help me find where to add a Duration Assertion in JMeter

Add think time between requests

Real users don't fire requests one after another with zero delay. They pause to read a page, fill out a form, or decide what to do next. In performance testing, this natural pause is called think time.

A Gaussian Random Timer adds a randomized delay between requests. This is more realistic than a fixed pause because real user behavior varies from request to request.

  • Right-click on the Pet Lifecycle Thread Group in the test plan tree.
  • Select Add, then Timer, then Gaussian Random Timer.
  • Set Deviation (in milliseconds) to 500.
  • Set Constant Delay Offset (in milliseconds) to 1000.

How does the Gaussian Random Timer work?

The timer adds a random delay before each sampler executes. The delay is calculated using a Gaussian (bell curve) distribution centered around the Constant Delay Offset.

With a Deviation of 500 and a Constant Delay Offset of 1000, most delays will fall between 0.5 and 1.5 seconds. This is far more realistic than every user pausing for exactly the same duration.

Why not a Constant Timer?

JMeter also offers a Constant Timer that adds a fixed delay (for example, exactly 1 second every time). This is simpler to configure but creates an unrealistic traffic pattern where all threads step in lockstep. The Gaussian Random Timer introduces natural variation that better represents real user behavior.

  • Confirm the Gaussian Random Timer appears as a direct child of the Pet Lifecycle Thread Group, alongside the Duration Assertion you added earlier.

Accidentally added the timer under a sampler?

If the Gaussian Random Timer is nested under a specific sampler (like Create Pet), it will only apply to that one request. Drag it up in the test plan tree so it sits directly under the Pet Lifecycle Thread Group. When placed at the Thread Group level, it applies to all samplers in the group.

Help me position my timer correctly in the JMeter test plan tree

Add a Response Assertion to the Delete request

Your Create Pet request already has a Response Assertion checking for a 200 status code. The Delete Pet request is the final step in your pet lifecycle, so it needs the same check to confirm the full workflow completed successfully.

  • Right-click on the Delete Pet sampler in the test plan tree.
  • Select Add, then Assertions, then Response Assertion.
  • In the Response Assertion panel, check the Response Code radio button under Field to Test.
  • Under Pattern Matching Rules, select Equals.
  • Click Add at the bottom of the Patterns to Test section and enter 200.

What does this do?

This assertion verifies that the Delete Pet request returns a 200 status code. If the API returns anything else (like a 404 because the pet was already deleted), JMeter marks that request as a failure in your results.

Why might a Delete return 404?

The Swagger Petstore is a shared public demo server. Other users might delete pets between your Create and Delete requests. Occasional 404 errors during a load test are expected on this shared server. In a real testing engagement, you'd be hitting a dedicated test environment where this wouldn't happen.

  • Confirm the Response Assertion appears nested under the Delete Pet sampler in the test plan tree (not at the Thread Group level).
  • Save your test plan by pressing Ctrl+S (Windows) or Cmd+S (macOS).

Response Assertion not appearing under Delete Pet?

Make sure you right-clicked on the Delete Pet sampler specifically, not on the Thread Group. A Response Assertion placed at the Thread Group level would check every single request for a 200 code, which might not be correct for all endpoints.

Help me add a Response Assertion to a specific sampler in JMeter

Your test plan tree under the Pet Lifecycle Thread Group should now show the Duration Assertion and Gaussian Random Timer alongside your samplers. The Delete Pet sampler should have a Response Assertion nested under it.

✔️ Awesome, my test plan tree looks right!

Great. Make sure you've saved your test plan before moving on.

ⓧ I'd like to double check the full test plan

Here is what your test plan tree should look like at this point. Verify each element is in the correct position.

  • Test Plan: Petstore Performance Suite

Your test plan now automatically catches slow responses and verifies that critical requests succeed. It also paces requests like a real user would. Next up, you'll run this test plan from the command line and generate a professional HTML dashboard report.

Run the Load Test from CLI and Generate the HTML Report

Your test plan now has assertions to catch failures automatically and timers to simulate realistic user behavior. It's time to put it all to the test under actual load.

JMeter's GUI is designed for building test plans, not running them. You'll switch to CLI mode for accurate results and generate a professional HTML Dashboard Report with performance metrics and graphs.

In this step, get ready to:
  • Save your test plan and prepare for CLI execution.
  • Run the load test from the command line in non-GUI mode.
  • Open and analyze the generated HTML Dashboard Report.
Save the test plan and prepare for CLI execution

For the CLI command to find your test plan and CSV data file, both files need to be in the same directory. You'll save the .jmx file to the same location as your pet_data.csv.

  • In JMeter, click File in the top menu bar.
  • Click Save Test Plan As.
  • Navigate to your JMeter bin directory (the same folder where you saved pet_data.csv).
  • Name the file petstore_performance.jmx and click Save.
  • Close the JMeter GUI.

Why close the GUI?

JMeter's official documentation explicitly states: "Don't run load test using GUI mode." The GUI consumes extra memory and CPU for rendering graphs and trees in real time.

That overhead skews your performance measurements. CLI mode strips the GUI away so every resource goes toward simulating virtual users accurately.

Now you need a terminal window open and pointed at your JMeter bin directory.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or cmd (Windows) and press Enter to open it.
  • Navigate to your JMeter bin directory by running this command:
cd [[JMETER_BIN="path/to/your/jmeter/bin"]]

Find your JMeter bin directory

If you installed JMeter by extracting the zip archive, the bin directory is inside the extracted folder. For example, C:\apache-jmeter-5.6.3\bin on Windows or ~/apache-jmeter-5.6.3/bin on macOS.

  • Confirm your test plan file is here by running:
ls petstore_performance.jmx

You should see petstore_performance.jmx listed in the output. If you also see pet_data.csv in the same directory, you're all set.

File not found?

If the ls command doesn't show your file, double check you saved the .jmx file to the correct directory. You can also run pwd to print your current directory and compare it to where you saved the file.

Help me find my petstore_performance.jmx file in the JMeter bin directory

Execute the load test in CLI mode

CLI mode runs your test plan without any graphical interface. Every system resource goes toward simulating your 5 virtual users instead of rendering trees and graphs.

  • Make sure a folder called report_output does not already exist in your current directory. JMeter requires the output folder to be new.
  • Run the load test and generate the HTML report by executing this command:
jmeter -n -t petstore_performance.jmx -l results.jtl -e -o report_output

What do these flags mean?

  • -n runs JMeter in non-GUI (CLI) mode.
  • -t specifies the test plan file to run.
  • -l sets the path for the results log file (.jtl format).
  • -e tells JMeter to generate the HTML Dashboard Report after the test finishes.
  • -o sets the output folder for the HTML report. This folder must not already exist.

JMeter prints a summary to the console every 30 seconds while the test runs. You should see lines showing throughput, average response time, and error percentage.

What should I see?

The console summary lines look something like this:

summary +     20 in 00:00:10 =    2.0/s Avg:   234 Min:   102 Max:   567 Err:     0 (0.00%)

The exact numbers will vary. When the test finishes, you'll see ... end of run in the output.

Seeing 'command not found' or 'is not recognized'?

On macOS or Linux, try running ./jmeter instead of jmeter. On Windows, make sure you are in the bin folder and try running jmeter.bat directly.

If you see a Java-related error, confirm Java is installed by running java -version. JMeter 5.6.3 requires Java 8 or higher (Java 17+ recommended).

If you see an error about the output folder already existing, delete the report_output folder and run the command again.

Help me troubleshoot my JMeter CLI load test command

Analyze the HTML Dashboard Report

JMeter generated a full HTML dashboard from your test results. This is the same type of report you would share with stakeholders on a real performance testing engagement.

  • Open report_output/index.html in your browser.

You should see a dashboard with multiple sections, tables, and graphs. Here are the key sections to focus on.

  • Scroll to the APDEX (Application Performance Index) table near the top. This shows how well each request's response time met your tolerance thresholds.
  • Find the Response Times Over Time graph. This shows how response times changed as your test progressed.
  • Check the Throughput section. This tells you how many requests per second the server handled.
  • Review the Response Time Percentiles chart. The 90th, 95th, and 99th percentile values are critical for SLA reporting.
  • Look at the Errors section. This breaks down any failed requests by type and shows you exactly what went wrong.

What is APDEX?

APDEX is a standardized score from 0 to 1 that measures user satisfaction based on response times. A score of 1.0 means every request was fast enough. A score below 0.5 means most users would consider the response unacceptably slow.

JMeter calculates this automatically using default tolerance thresholds. It's one of the first things a performance tester looks at to get a quick read on overall health.

Now that you've reviewed the sections, think about what the data tells you.

  • Check whether any requests are consistently slower than others by comparing the average response times per sampler.
  • Look for any Duration Assertion failures in the Errors table. These indicate requests that took longer than your 3-second SLA threshold.
  • Check whether throughput stayed stable throughout the test or started degrading toward the end.

Seeing some errors in your report?

The Swagger Petstore is a shared public demo server. Other users may be creating and deleting pets at the same time as your test. This means occasional 404 errors on GET or DELETE requests are normal if another user deleted the pet between your requests.

A small error rate (under 5%) on a shared demo server is expected. On a dedicated test environment, you would aim for 0% errors.

You've gone from building a test plan to running it like a professional and analyzing real results. Your next challenge takes it further. In the Secret Mission, you'll configure a spike test to see how the API handles a sudden burst of 50 users all at once.

Secret mission

Configure a Spike Test

Your steady-state load test tells you how the API performs under predictable traffic. But what happens when 50 users hit the API all at once? Configure a spike test using JMeter's Synchronizing Timer and compare the results against your baseline.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using JMeter and a public demo API, so there are no ongoing costs. Decide whether to keep your test files for future use, or delete them entirely.

Resources you used:

  • JMeter test plan: petstore_performance.jmx
  • CSV data file: pet_data.csv
  • Results log file: results.jtl
  • HTML Dashboard Report folder: report_output/
  • Spike test results (if you completed the Secret Mission): spike_results.jtl and spike_report/

Keep everything running

No action needed. Choose this if you plan to continue refining your test plan or running additional load tests.

  • Your .jmx test plan is ready to re-run any time from the CLI.
  • Your CSV data file and results are all stored locally.
  • The Petstore API is a public demo server that requires no authentication, so your test plan will continue working without any account maintenance.

Pause - I'll come back to this later

Delete the generated output files to free up disk space while keeping your test plan and data files intact.

  • Delete the report_output/ folder and results.jtl file. These are regenerated every time you run the test.
  • If you completed the Secret Mission, also delete spike_report/ and spike_results.jtl.
  • Keep petstore_performance.jmx and pet_data.csv. These are all you need to re-run the full test suite later.

Delete - I don't want to use this again

Remove all project files to start fresh.

  • Delete petstore_performance.jmx.
  • Delete pet_data.csv.
  • Delete results.jtl and the report_output/ folder.
  • If you completed the Secret Mission, delete spike_results.jtl and the spike_report/ folder.
  • JMeter itself can remain installed for future projects. If you want to remove it, simply delete the JMeter installation folder.

Nice Work!

Nice Work!

You just built a production-quality performance test suite for the Swagger Petstore API using Apache JMeter. This is the kind of test plan you would deliver on a real performance testing engagement.

You've learned how to:

  • Built a correlated API workflow that chains requests using JSON Extractor to pass dynamic data between samplers.
  • Implemented CSV-driven parameterization for realistic, varied test data across concurrent threads.
  • Executed load tests from CLI mode and generated a professional HTML Dashboard Report with APDEX scores, percentile charts, and throughput graphs.
  • Configured a spike test with a Synchronizing Timer to simulate sudden traffic bursts and compared degradation against baseline metrics.

Ready to quiz yourself?