Resource library

QA How-To

REST Assured Response Time Assertions Tutorial

Learn REST Assured response time assertions with runnable Java tests, explicit time units, warmup samples, per-route budgets, and CI verification steps.

25 min read | 3,469 words

TL;DR

REST Assured checks a completed response with `.then().time(lessThan(2000L))`, where the default unit is milliseconds. Pair the timing matcher with status and body assertions, warm the JVM for comparisons, and use a load tool for throughput or tail-latency claims.

Key Takeaways

  • Use `.then().time(lessThan(2000L))` for a millisecond response-time ceiling.
  • Assert status and body alongside latency so a fast error does not pass a success test.
  • Specify `TimeUnit.MILLISECONDS` when the unit should be unmistakable.
  • Warm the JVM before comparing measured response times.
  • Use `response.timeIn` for diagnostic messages and per-scenario budgets.
  • Treat small sequential samples as smoke checks, not load-test percentiles.

REST Assured Response Time Assertions let you fail a Java API test when a completed HTTP exchange exceeds a chosen duration. The shortest form is then().time(lessThan(2000L)), which checks milliseconds. In this tutorial you will build a local endpoint, assert its status and payload alongside its time, inspect measured values, and choose a threshold that does not turn ordinary CI variation into false alarms.

A response time assertion is a useful regression guard for a specific request path. It is not a load test or a measurement of server processing time alone. REST Assured measures the client side request and response work, including the HTTP round trip and its own processing. The REST Assured usage guide explicitly recommends measuring after the JVM is warm. You will account for that distinction instead of treating one cold run as a production latency claim.

This walkthrough uses an in-process JDK HTTP server. There is no remote demo service, login, or shared test account. The endpoint has an optional, controlled delay so you can see a passing assertion, diagnose a failing one, and learn what a duration measurement does and does not prove. If you are new to the given, when, then style, the REST Assured given-when-then guide provides the broader request DSL.

What You Will Build

  • A Maven test project with Java, JUnit Jupiter, and REST Assured.
  • A loopback HTTP endpoint whose response body reports a small requested delay.
  • Direct response time assertions in milliseconds and with an explicit TimeUnit.
  • A measurement test that warms the JVM, collects samples, and checks a median.
  • Separate checks for a successful response and a fast error response.
  • A parameterized budget test and a CI command that writes Surefire reports.

The delay is a teaching control, not a simulated benchmark. A 120 ms server sleep adds at least that much wait to the client request, but scheduling, network stack work, and REST Assured processing also contribute. The example thresholds leave room for a normal development machine. Replace them with limits derived from your product's service objectives before applying the pattern to a real API.

Prerequisites

Use JDK 21 and Apache Maven 3.10.0 for the commands shown here. The sample pom.xml pins REST Assured 6.0.1, JUnit Jupiter 6.1.3, Maven Compiler Plugin 3.14.1, and Maven Surefire Plugin 3.5.5. These are published versions documented by the REST Assured project, JUnit project, and Maven project. REST Assured 6 requires Java 17 or newer, so JDK 21 meets that baseline. If your organization uses other approved versions, match the versions installed in its build image and check their compatibility before changing the pins.

java -version
mvn -version

Both commands should report the JDK Maven actually uses. On a machine with several JDKs, java -version and the Java version line in mvn -version can differ because Maven reads JAVA_HOME. Correct that mismatch before debugging test failures. The tutorial assumes a shell with mkdir and Maven on PATH; on Windows, run the equivalent directory command in PowerShell and keep the same Maven goals.

You should already know how an HTTP status code differs from a JSON field. The REST Assured tutorial for beginners covers request building if those basics are unfamiliar. No application server is installed for this exercise: Java's HttpServer starts inside the test process and binds to a free loopback port.

Step 1: Create the Maven project

Create a clean directory and the standard Maven test source path. Save the XML as pom.xml in the new directory. The only runtime code will be the JDK HTTP server started from a test class; REST Assured and JUnit belong in test scope.

mkdir -p response-time-assertions/src/test/java/example
cd response-time-assertions
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>response-time-assertions</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <version>6.0.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.1</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.5</version>
      </plugin>
    </plugins>
  </build>
</project>

Verify the project definition with mvn -q -DskipTests package. It should exit successfully after Maven resolves dependencies. This command deliberately does not claim a test passed; there is no test class yet. If dependency resolution fails behind a corporate proxy, configure Maven's approved repository mirror in your local settings rather than replacing the documented coordinates with an invented version. Pinning the compiler and test plugin also prevents differences caused by Maven selecting older defaults.

The XML dependency order puts REST Assured before JUnit, as the REST Assured getting-started guide recommends for Hamcrest dependency selection. REST Assured exposes the time assertion DSL, while JUnit supplies lifecycle hooks and test execution. The project does not require a separate JSON library in your code because REST Assured can parse the small JSON responses used below.

Step 2: Start an isolated endpoint

Save the following complete class as src/test/java/example/ResponseTimeTest.java. It starts one server for the class, gets a free port from the operating system, and stops the server after the tests. /health answers immediately. /delayed?ms=120 sleeps for the requested number of milliseconds, restricted to 0 through 500, then returns JSON. /missing returns a 404 for a separate negative case.

package example;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import io.restassured.response.Response;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ResponseTimeTest {
    private static HttpServer server;
    private static String baseUrl;

    @BeforeAll
    static void startServer() throws IOException {
        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
        server.createContext("/health", exchange -> respond(exchange, 200, "{\"status\":\"ok\"}"));
        server.createContext("/missing", exchange -> respond(exchange, 404, "{\"error\":\"not found\"}"));
        server.createContext("/delayed", exchange -> {
            String query = exchange.getRequestURI().getRawQuery();
            int delay;
            try {
                if (query == null || !query.startsWith("ms=")) {
                    throw new NumberFormatException("missing ms");
                }
                delay = Integer.parseInt(query.substring(3));
                if (delay < 0 || delay > 500) {
                    throw new NumberFormatException("ms outside 0..500");
                }
            } catch (NumberFormatException error) {
                respond(exchange, 400, "{\"error\":\"invalid ms\"}");
                return;
            }
            try {
                Thread.sleep(delay);
            } catch (InterruptedException error) {
                Thread.currentThread().interrupt();
                respond(exchange, 503, "{\"error\":\"interrupted\"}");
                return;
            }
            respond(exchange, 200, "{\"delayMs\":" + delay + "}");
        });
        server.start();
        baseUrl = "http://127.0.0.1:" + server.getAddress().getPort();
    }

    @AfterAll
    static void stopServer() {
        if (server != null) {
            server.stop(0);
        }
    }

    private static void respond(HttpExchange exchange, int status, String json) throws IOException {
        byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        exchange.sendResponseHeaders(status, bytes.length);
        try (OutputStream output = exchange.getResponseBody()) {
            output.write(bytes);
        }
    }

    @Test
    void healthContractIsAvailable() {
        given().baseUri(baseUrl)
            .when().get("/health")
            .then().statusCode(200).body("status", equalTo("ok"));
    }
}

Verify with mvn -q -Dtest=ResponseTimeTest test. Expect one test and zero failures; target/surefire-reports contains the detailed result. The imports for later steps are present from the start, so each later method can be pasted immediately before the final } of this class without editing names or signatures. Unused imports at this stage are harmless to javac.

The server accepts only loopback traffic, so another machine cannot reach it. Port zero avoids conflicts with an existing service and makes the test suitable for parallel build agents. If this first request fails with connection refused, check that server.start() ran in @BeforeAll and that your test uses baseUrl, not a hard-coded port. The health test establishes functional correctness before any latency threshold is introduced.

Step 3: Add REST Assured Response Time Assertions to a successful request

Paste this method before the class's closing brace. REST Assured's time(Matcher<Long>) overload interprets the matcher in milliseconds. Hamcrest's lessThan receives a Long, so use the L suffix. A status and body check accompany the timing assertion so a fast error page cannot count as a successful health request.

@Test
void healthRespondsWithinTwoSeconds() {
    given().baseUri(baseUrl)
        .when().get("/health")
        .then()
        .statusCode(200)
        .body("status", equalTo("ok"))
        .time(lessThan(2000L));
}

Verify with mvn -q -Dtest=ResponseTimeTest#healthRespondsWithinTwoSeconds test. Expect one passing test. Then temporarily change the threshold to lessThan(0L) and run the same command to see the failure message. Restore 2000L afterward. This controlled failure shows that the matcher is actually connected to the response timing; it does not establish a useful service objective.

Use a threshold that represents a documented expectation for a route, environment, and request size. A 2-second ceiling is illustrative for this local exercise, not a universal API standard. In a real service, auth, cache state, payload size, geographic route, and dependency calls can change the expected latency. Place those factors in the test setup or test name so a future maintainer knows what the number covers. The API performance testing tutorial explains broader measurement planning.

The assertion measures the exchange REST Assured performed. It does not separately report DNS lookup, TCP connection, server queueing, or database time. For root cause analysis, correlate a failing test with client logs, server traces, and service metrics. A response time assertion is a tripwire: it tells you where to investigate, not which component is slow.

Step 4: Name the unit and inspect the measured value

An explicit TimeUnit prevents readers from mistaking 2000 for seconds. This step checks a delayed endpoint and captures the measured duration for a useful failure message. Response.timeIn(TimeUnit.MILLISECONDS) returns a long; according to REST Assured's response API it can return -1 if timing was unavailable, so the test checks for a nonnegative value before printing it.

@Test
void delayedResponseReportsMilliseconds() {
    Response response = given().baseUri(baseUrl)
        .when().get("/delayed?ms=120");

    response.then()
        .statusCode(200)
        .body("delayMs", equalTo(120))
        .time(lessThan(2000L), TimeUnit.MILLISECONDS);

    long elapsedMs = response.timeIn(TimeUnit.MILLISECONDS);
    assertTrue(elapsedMs >= 0, "REST Assured did not record response time");
    System.out.println("Observed client duration: " + elapsedMs + " ms");
}

Verify with mvn -Dtest=ResponseTimeTest#delayedResponseReportsMilliseconds test. Expect a passing test and an Observed client duration line. The printed number is likely above 120 ms because the client and server have work beyond the sleep; it should not be treated as a precise performance baseline. If your build suppresses standard output, inspect the Surefire text report or rerun without -q.

The REST Assured response time documentation also demonstrates time(lessThan(2L), SECONDS). Prefer milliseconds when the budget is below a second or needs precise boundaries. Converting a long millisecond duration to whole seconds can discard fractional detail. A response.time() call is another millisecond form, useful when you only want a measured value; .then().time(...) communicates the budget directly in the assertion DSL.

Do not confuse this assertion with a client timeout. A response that takes 2.1 seconds can still complete and then fail the matcher. A socket or connection timeout instead aborts the exchange and may leave no response object to validate. Configure transport timeouts separately for production test clients so a hung request cannot block the suite indefinitely.

Step 5: Warm up and check a sample median

One cold JVM request can pay class loading, JIT compilation, and connection setup costs. This example runs ten untimed warmup calls, records nine later responses, sorts those durations, and checks the middle observation. It is intentionally a small stability exercise, not a statistical latency study. Add the following method to the same class.

@Test
void medianOfWarmResponsesStaysBelowBudget() {
    for (int i = 0; i < 10; i++) {
        given().baseUri(baseUrl)
            .when().get("/delayed?ms=120")
            .then().statusCode(200);
    }

    long[] samples = new long[9];
    for (int i = 0; i < samples.length; i++) {
        Response response = given().baseUri(baseUrl)
            .when().get("/delayed?ms=120");
        response.then().statusCode(200).body("delayMs", equalTo(120));
        samples[i] = response.timeIn(TimeUnit.MILLISECONDS);
        assertTrue(samples[i] >= 0, "Missing timing sample " + i);
    }

    Arrays.sort(samples);
    long medianMs = samples[samples.length / 2];
    assertTrue(medianMs < 800,
        () -> "Warm median was " + medianMs + " ms; budget is 800 ms");
}

Verify with mvn -q -Dtest=ResponseTimeTest#medianOfWarmResponsesStaysBelowBudget test. Expect one pass after nineteen HTTP calls. The loop has ten warmups plus nine recorded samples; no arbitrary wall-clock sleep is needed before the measurement. The server itself sleeps 120 ms for each call, so this test takes at least about 2.3 seconds overall. The generous 800 ms median budget is chosen for a local educational test, and a heavily loaded laptop or CI agent can still breach it.

A median answers a different question from a hard maximum. If one request spikes while eight remain fast, the median may pass. For a strict per-request service objective, retain direct time(lessThan(...)) checks. For tail latency, use enough independently collected samples to estimate a percentile and define the workload, concurrency, and confidence you need. Nine sequential requests cannot establish a meaningful p95 under load. The performance testing with k6 scripts guide is a better starting point for controlled concurrent traffic.

Do not add retries that silently replace a slow observation. Retries change what is measured and may hide an intermittent regression. If a sample fails, preserve the route, environment, timestamp, and observed value. Repeat a diagnostic run deliberately, then look for resource contention or a real service change. A stable local median says only that this local setup behaved within its chosen budget during that run.

Step 6: Keep correctness and speed together on error paths

A fast 404 is still wrong when the contract expects 200. Conversely, a documented 404 may deserve its own latency limit. Add this method to test an intentional missing-resource response. The JSON error field identifies the failure branch, and the time matcher guards that branch's client-visible duration.

@Test
void missingResourceHasItsOwnBudget() {
    given().baseUri(baseUrl)
        .when().get("/missing")
        .then()
        .statusCode(404)
        .body("error", equalTo("not found"))
        .time(lessThan(2000L), TimeUnit.MILLISECONDS);
}

Verify with mvn -q -Dtest=ResponseTimeTest#missingResourceHasItsOwnBudget test. Expect one pass. Change the expected status to 200 briefly to confirm that the test fails for status even when the duration is acceptable, then restore 404. The order of fluent assertions is not a substitute for checking all contract dimensions; each asserted condition matters.

A real negative path may be slower than a cache hit because it consults several stores or authorization policies. Do not copy the success-path budget without checking the service objective for that route. Also avoid using a timing assertion to explain a 401, 403, or 500: first verify why the response differs. The API error handling and negative testing guide helps build a status and body matrix before you add latency limits.

The local /missing endpoint always returns the same 404. A production endpoint might return 404 for missing records and 403 for unauthorized records, depending on its disclosure policy. Assert the documented status and message for the identity used in the test. If authentication is required, include it in the request specification, and keep secrets out of logs and CI artifacts.

Step 7: Apply budgets to several request shapes

Parameterized tests make the expected delay and budget visible together. They are useful when one route has a few well-defined workload classes, such as a small and large fixture. Here, all three requests are comfortably below a 1-second demonstration budget. Paste the method into the class; junit-jupiter already provides the parameterized test engine.

@ParameterizedTest(name = "delay {0} ms stays below {1} ms")
@CsvSource({"0,1000", "120,1000", "250,1000"})
void delayedRequestsMeetTheirBudgets(int delayMs, long budgetMs) {
    Response response = given().baseUri(baseUrl)
        .queryParam("ms", delayMs)
        .when().get("/delayed");

    response.then()
        .statusCode(200)
        .body("delayMs", equalTo(delayMs));

    long observedMs = response.timeIn(TimeUnit.MILLISECONDS);
    assertTrue(observedMs >= 0 && observedMs < budgetMs,
        () -> "Delay " + delayMs + " ms took " + observedMs
            + " ms; budget is " + budgetMs + " ms");
}

Verify this step with mvn -q -Dtest=ResponseTimeTest#delayedRequestsMeetTheirBudgets test. Expect three passes. Run the complete class with mvn test; expect eight tests and zero failures: five ordinary methods plus three parameterized cases. Surefire writes XML and text reports to target/surefire-reports. In CI, archive those reports and compare a slow failure with machine load, route behavior, and recent changes. The REST Assured request and response spec guide shows how to share common headers and contract checks as the suite grows.

The queryParam call lets REST Assured encode the query rather than concatenating a string for every case. For a production workload, parameter rows should represent meaningful input sizes, permissions, or response shapes. Do not make a huge matrix by merely enumerating delays: the server's sleep is only a controlled demonstration. Budget values belong beside their scenario or in a documented configuration so reviewers can tell why each number exists.

This final method uses Response.timeIn and JUnit's assertTrue instead of .then().time(...) because it needs the expected limit in a custom message. Both use the recorded REST Assured duration. Use the fluent matcher when one fixed threshold tells the story clearly; use an extracted value when you need calculations, per-case budgets, or diagnostic detail. Do not call the endpoint twice just to obtain a measurement after an assertion. A second call is a different observation.

How to read REST Assured Response Time Assertions

Check What it answers What it cannot establish
.time(lessThan(2000L)) Did this completed client request finish under 2,000 ms? Which component used the time
response.timeIn(MILLISECONDS) What duration did REST Assured record for this exchange? A server-only processing duration
Warm nine-sample median Was the middle observation below a chosen local limit? Tail latency or behavior under concurrency
Transport timeout Did the client abandon a stalled exchange? Whether a completed response met its latency objective

REST Assured's timing includes work outside your service method. That is appropriate for a user-facing guard, but it means server logs and distributed traces can show a smaller number. Check the same request ID, route, payload, and environment when comparing tools. A monitoring dashboard may use p95 over thousands of requests; a JUnit test usually covers a tiny, sequential sample. They answer different operational questions.

For repeatable comparisons, keep the network route and runner class stable. Record whether the first call is cold, whether caches were primed, and whether another test is generating load. CI machines can have noisy neighbors, so a very tight threshold around normal latency will flap. If your service objective is strict, run a dedicated performance job with controlled load rather than diluting the objective until ordinary CI passes.

Troubleshooting

Problem: time(lessThan(2000)) fails to compile -> Use 2000L. The matcher expects a Long response duration; 2000 is an Integer. Import lessThan from org.hamcrest.Matchers, which REST Assured brings into this test dependency graph.

Problem: the first request is much slower than later ones -> Run the warmup calls and inspect recorded samples separately. JVM class loading, JIT activity, and new connections can dominate a single cold observation. If cold start is itself a product requirement, test it in a dedicated cold-start scenario instead of mixing it with warm requests.

Problem: a request exceeds the budget only in CI -> Compare the build agent's CPU contention, Java version, network path, and concurrent jobs with your local run. Use Surefire's per-test report to identify the failing case. Keep the threshold tied to a stated objective rather than automatically increasing it after every red build.

Problem: connection refused appears instead of a timing failure -> Confirm @BeforeAll started the server and the request uses the port returned by server.getAddress().getPort(). A transport exception occurs before REST Assured can validate status or elapsed response time. Check the server startup error first.

Problem: a 404 meets the time budget but the test still fails -> Read the status and body assertion. A fast error cannot satisfy a success contract. For expected negative cases, write a separate test with the documented error status and its own duration limit.

Problem: timeIn(SECONDS) prints zero for a fast request -> Whole-unit conversion loses subsecond detail. Use TimeUnit.MILLISECONDS for small budgets and keep the matcher literal's unit explicit. Avoid inferring zero latency from a truncated whole-second value.

Where To Go Next

Move one stable endpoint from this local example into a real test environment. Define the expected status and body first, then choose a latency budget for that exact request shape. Record the agreed service objective, the environment, and the traffic assumptions near the test. Use a shared request specification for auth and base URI, but let each route own its response-time expectation.

When the team needs concurrency, throughput, or p95/p99 evidence, use a dedicated load tool and a controlled environment. A JUnit timing assertion can stay in the fast API regression suite as an early signal, while a load job measures performance under a representative workload. The API testing roadmap places these checks in a broader automation plan, and REST Assured logging filters can help capture request and response context without turning every passing CI run into noise.

Interview Questions and Answers

Q: What does REST Assured include in response.time()?

It records the client-side duration for the request and consumed response, including HTTP round-trip and REST Assured work. I would not present it as a server-only processing metric. To isolate server time, I would compare it with server instrumentation or tracing for the same request.

Q: Why is the L suffix important in lessThan(2000L)?

REST Assured's timing matcher compares a Long value. The suffix makes Hamcrest's matcher operate on the same numeric type. Without it, Java generic typing can reject the assertion or produce a confusing mismatch.

Q: Why assert status and body with a latency budget?

An error response can be very fast. If the test checks only time, a 500 or 404 may look like success. I verify the intended functional branch before interpreting its latency.

Q: Would you use one response time limit for all endpoints?

No. A cached health check and a report generation route have different work and objectives. I choose limits for each documented scenario and environment, then review them when dependencies or payloads change.

Q: Is a nine-request median enough for a p95 claim?

No. It is a small sequential smoke sample with almost no information about tail behavior under load. A p95 claim needs a defined workload, many observations, and an appropriate analysis of variability.

Q: How is a response time assertion different from a timeout?

The assertion evaluates a response after it completes. A timeout stops waiting during connection or reading and may leave no response to inspect. Both can be useful, but they protect against different failures.

These short model answers focus on what the API measures and what evidence is missing. In an interview, explain the workload and environment before quoting a threshold.

Common Mistakes

  • Treating a single cold request as a stable baseline for all future builds.
  • Copying an example's 2-second limit into a product with a different service objective.
  • Using seconds for subsecond budgets and losing the distinction between 100 ms and 900 ms.
  • Checking time without the expected HTTP status and payload branch.
  • Retrying failed observations automatically and reporting only the fastest attempt.
  • Calling the endpoint again to measure it after the functional assertion, then treating both calls as one result.
  • Describing REST Assured's duration as server execution time or as a load-test percentile.

A good timing test makes its request shape, environment, expected response, and budget visible. When it fails, preserve the measured value and investigate the route rather than deleting the assertion. If the scenario is too noisy for a fast CI gate, move it to a controlled performance job with an explicit owner and reporting path.

Conclusion

REST Assured Response Time Assertions are straightforward to write: assert the response contract, then add .time(lessThan(limitInMilliseconds)) or an explicit TimeUnit. Extract response.timeIn(MILLISECONDS) when you need a per-case message or a small collection of observations. Warm the JVM before comparing runs, and remember that the recorded duration is client visible, not server only.

Run the eight-test local suite, force one deliberate threshold failure, and inspect the Surefire report. Then apply the same pattern to one real endpoint with a documented budget. Keep load and tail-latency claims in a dedicated performance test where you can control the workload and explain the resulting numbers.

Interview Questions and Answers

What does REST Assured's `time` matcher validate?

It applies a Hamcrest matcher to the recorded duration of a completed HTTP exchange. Without a unit argument, that duration is in milliseconds. I pair it with status and payload assertions to ensure I timed the intended response branch.

Why write `lessThan(2000L)` instead of `lessThan(2000)`?

The measured time is a Java `long`, so Hamcrest needs a matcher for `Long`. The `L` literal suffix makes the expected type clear and avoids a generic type mismatch. The numeric limit still has to come from a documented objective.

How would you distinguish client-observed and server processing time?

REST Assured records client-observed duration, which includes network and client processing. I would use a request ID to correlate that observation with server traces or metrics. The difference helps reveal network, queueing, or client-side overhead.

What causes a response time assertion to flap in CI?

A limit close to ordinary variation can fail when the shared runner has CPU contention, a cold JVM, or a different network route. I would preserve failed observations, compare the environment and workload, and decide whether the check belongs in a controlled performance job. Blindly increasing the number can hide a real regression.

Why is a fast 500 response not a passing latency test?

The request did not meet its functional contract. A timing assertion alone proves only that an HTTP response arrived within a duration. I assert the expected status and body before interpreting the speed of that branch.

How do you test a latency objective for error responses?

I create an intentional negative request, assert its documented status and error body, and apply a separate budget to that path. Error handling can perform different work from successful calls, so I do not assume the same limit. I also distinguish an HTTP error from a transport exception with no response.

When would you use `response.timeIn` instead of `.then().time(...)`?

I use `timeIn` when a parameterized test needs a case-specific budget or when a custom failure message should show the observed value. The fluent matcher is concise for a fixed limit. I avoid issuing a second request just to retrieve a number.

Frequently Asked Questions

How do I assert response time in REST Assured?

Chain `.then().time(lessThan(2000L))` after the request to require a duration below 2,000 milliseconds. Add status and body assertions in the same chain so a quick error response cannot satisfy a success case.

What unit does REST Assured use for `time(lessThan(...))`?

The one-argument timing matcher uses milliseconds. You can write `.time(lessThan(2000L), TimeUnit.MILLISECONDS)` when an explicit unit makes the test easier to review.

How can I print the response time in a test failure?

Keep the returned `Response`, read `response.timeIn(TimeUnit.MILLISECONDS)`, and include that value in a JUnit assertion message. Check for a nonnegative measurement before comparing it with a budget.

Does `response.time()` measure only server processing?

No. It covers the REST Assured client-side request and consumed response, including the HTTP round trip and client processing. Use server metrics or traces when you need a server-only duration.

Should I warm up before asserting API latency?

Warmup is helpful when comparing measurements because the first JVM request may include class loading, JIT work, and connection setup. If cold-start behavior matters to users, test it as its own scenario rather than silently discarding it.

Can a response time assertion replace a load test?

No. A single or small sequential group of requests does not establish throughput or p95 latency under concurrency. Use a dedicated performance tool with a defined workload for those claims.

Is a REST Assured timing assertion the same as an HTTP timeout?

No. A timing assertion fails after a completed response exceeds the limit. A connection or read timeout interrupts waiting and may leave no HTTP response to assert.

Related Guides