--- title: "Fast Tests: Startup Time, Memory and Reuse" shortTitle: "Fast Tests" description: "Keep MockServer tests fast: start one container per suite and reset() between tests for millisecond per-test cost, run MockServer in-process for JVM suites, bound container memory, and pick the right image variant for the fastest startup." layout: page pageOrder: 5 section: 'Performance Testing' subsection: true sitemap: priority: 0.7 changefreq: 'monthly' lastmod: 2026-07-02T09:00:00+01:00 keywords: mockserver fast tests, mockserver testcontainers reuse, mockserver reset between tests, mockserver in-process, mockserver container memory limit, mockserver startup time schema_faq: - question: "What is the fastest way to run MockServer in tests?" answer: "Start one MockServer container per test suite and call client.reset() between tests — reset() clears all expectations and recorded requests in milliseconds, giving the same isolation as a fresh container without the startup cost per test. For JVM test suites that need no network isolation, running MockServer in-process (mockserver-netty or the JUnit 5 extension) is faster still: it starts in tens of milliseconds with no Docker overhead." - question: "How do I limit MockServer's memory in tests?" answer: "Set a container memory limit — the image caps the JVM heap at 75% of the container limit (-XX:MaxRAMPercentage=75.0), so limiting the container to 256 MiB caps the heap at ~192 MiB. To set an explicit heap size instead, pass -Xmx via the JAVA_TOOL_OPTIONS environment variable; note that overriding -XX:MaxRAMPercentage via JAVA_TOOL_OPTIONS has no effect because the image applies its own value afterwards." - question: "Does MockServer support GraalVM native-image?" answer: "No, and there are no plans to. MockServer loads user-supplied classes at runtime (class callbacks, expectation initializers, custom matchers), which is incompatible with native-image's closed-world model — in testing, features not explicitly exercised during build-time metadata collection failed silently at runtime. For faster container startup use the -aot image tags (JDK 25 Leyden AOT cache, ~0.35 s to ready); for JVM-less environments use the jlink binary bundle." ---

There is a clear performance hierarchy for MockServer in tests. The biggest win is architectural: start one container per suite and call client.reset() between tests — this brings per-test cost down to milliseconds regardless of how fast the container starts. The next step up is avoiding the container entirely for JVM tests by running MockServer in-process, which eliminates Docker overhead altogether. Only after those two are in place does runtime-level startup optimisation — faster base images, AOT caches — make a meaningful difference.

The examples below use the Java Testcontainers module; the same suite-scoped container and reset-between-tests pattern applies to every language the Testcontainers modules support.

 

Startup is fast

The mockserver/mockserver container image starts quickly — it reaches ready in roughly 0.57 s on a typical developer machine, thanks to a class-data-sharing archive baked into the image at build time. If startup appears to take several seconds, check that the image version being pulled matches the client library on the classpath and that the wait strategy in use polls for readiness rather than sleeping for a fixed interval. For a full breakdown of startup times and options, see Start-up Time.

 

One container per suite, reset between tests

The most effective way to keep tests fast is to start a single MockServer container once for the whole test class (or suite) and call client.reset() between tests. reset() clears all expectations and recorded requests in milliseconds, giving the same isolation as a fresh container without paying the startup cost on every test.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.testcontainers.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

@Testcontainers
class SuiteReuseExampleTest {

    @Container
    static MockServerContainer mockServer = new MockServerContainer();

    static MockServerClient client;

    @BeforeAll
    static void startClient() {
        client = mockServer.getClient();
    }

    @AfterEach
    void resetBetweenTests() {
        client.reset(); // clears expectations and recorded requests in milliseconds
    }

    @Test
    void shouldMockFirstEndpoint() {
        client
            .when(request().withPath("/first"))
            .respond(response().withBody("one"));
        // point the system under test at mockServer.getEndpoint()
    }

    @Test
    void shouldMockSecondEndpoint() {
        client
            .when(request().withPath("/second"))
            .respond(response().withBody("two"));
        // point the system under test at mockServer.getEndpoint()
    }
}

Testcontainers' reuse feature (withReuse(true) on the container, plus testcontainers.reuse.enable=true in ~/.testcontainers.properties) keeps the container alive across test runs. Combined with client.reset() between tests, this eliminates startup cost entirely for local development iterations. When using withReuse(true), also call client.reset() at the start of the suite (for example in @BeforeAll) so any state left behind by an aborted previous run is cleared before the first test executes.

 

Bounding memory

The image caps the JVM heap at 75% of the container memory limit (-XX:MaxRAMPercentage=75.0). Setting a container memory limit is the supported way to control MockServer's footprint in your test environment. Without a limit, idle footprint is roughly 200 MiB (measured at idle with no active requests and no explicit memory limit).

// Limit MockServer to 256 MiB of container memory (heap capped at ~192 MiB)
MockServerContainer mockServer = new MockServerContainer()
    .withCreateContainerCmdModifier(cmd ->
        cmd.getHostConfig().withMemory(256 * 1024 * 1024L));

To override the percentage entirely, set an explicit heap size via JAVA_TOOL_OPTIONS:

MockServerContainer mockServer = new MockServerContainer()
    .withEnv("JAVA_TOOL_OPTIONS", "-Xmx128m");

Note: setting a different -XX:MaxRAMPercentage via JAVA_TOOL_OPTIONS has no effect — the image applies its own value after JAVA_TOOL_OPTIONS, so it always wins. Use an explicit -Xmx instead, which takes precedence over the percentage cap.

 

Fastest option for Java tests: run MockServer in-process

For JVM test suites, starting MockServer inside the test JVM is faster than any container variant because the JVM is already warm and no Docker daemon round-trip is required. There are two ways to do it:

Maven

<!-- programmatic start -->
<dependency>
    <groupId>org.mock-server</groupId>
    <artifactId>mockserver-netty</artifactId>
    <version>{{ site.mockserver_version }}</version>
    <scope>test</scope>
</dependency>

<!-- or: JUnit 5 extension (brings in mockserver-netty transitively) -->
<dependency>
    <groupId>org.mock-server</groupId>
    <artifactId>mockserver-junit-jupiter</artifactId>
    <version>{{ site.mockserver_version }}</version>
    <scope>test</scope>
</dependency>

Programmatic start (random port, no Docker):

import org.mockserver.integration.ClientAndServer;

// Starts in tens of milliseconds — no Docker required.
// Pass 0 to let MockServer pick a free port automatically.
ClientAndServer client = ClientAndServer.startClientAndServer(0);
int port = client.getLocalPort();

// … set up expectations and run tests …

client.stop();

Trade-offs

In-process (mockserver-netty / JUnit 5 extension)Testcontainers
No Docker requiredRequires Docker
Starts in tens of milliseconds~0.57 s cold start (or zero with reuse)
Shares the test JVM classpath — class callbacks and custom matchers work naturallyFull network isolation — the mock server runs in a separate process
Best for single-language JVM test suitesRequired for non-JVM languages; preferred for multi-service integration tests where network isolation matters
 

Runtime-level startup options

The following figures are measured on a typical developer machine (macOS arm64). Results vary by machine and workload; treat these as order-of-magnitude guidance, not guarantees. For a full breakdown see Start-up Time.

OptionTime-to-ready (typical)Status
Standard image (distroless JDK 17 + class-data sharing archive) ~0.57 s Default. Class-data-sharing archive baked in at image build time; identical runtime behaviour to a plain JVM start.
Project Leyden AOT cache (JDK 25, JEP 483/514) ~0.35 s Available as opt-in -aot image tags; see below.
GraalVM native-image ~72 ms (~8× faster) Not supported — see below.

Project Leyden AOT cache (JDK 25, JEP 483/514)

Measured roughly 40% lower container time-to-ready (~0.35 s vs ~0.57 s for the standard image) with identical behaviour. Because it is the real HotSpot JVM, there is 100% feature parity — no closed-world limitations, no missing features at runtime.

The AOT variant is published as opt-in image tags to Docker Hub and ECR Public — mockserver/mockserver:<version>-aot and mockserver/mockserver:latest-aot. In a JVM test you can select it through the MockServer Testcontainers module:

// aotImage() = the default resolved image with -aot appended to its tag,
// so it stays in lockstep with the client library version.
try (MockServerContainer mockServer = new MockServerContainer(MockServerContainer.aotImage())) {
    mockServer.start();
    // ... use mockServer.getClient() ...
}

// Or pin an explicit -aot tag (both X.Y.Z-aot and mockserver-X.Y.Z-aot
// tag forms are published; aotImage() resolves the mockserver-X.Y.Z-aot form):
try (MockServerContainer mockServer = new MockServerContainer(
        DockerImageName.parse("mockserver/mockserver:7.6.0-aot"))) {
    mockServer.start();
}

The variant is also defined at docker/aot/Dockerfile in the MockServer repository, so you can build it yourself for local evaluation. It jlink-trims a JDK 25 runtime, starts MockServer during the image build as a training run, and bakes the resulting AOT cache into the image:

# The build context requires ca-bundle.pem (may be empty — it is only needed behind a
# corporate TLS-inspection proxy; see the Dockerfile comments).
cd docker/aot && touch ca-bundle.pem && docker build .

# Pin a specific MockServer version:
cd docker/aot && touch ca-bundle.pem && docker build --build-arg VERSION=7.6.0 .

Why it is not the default image:

GraalVM native-image

Measured ~10× faster to ready (~72 ms) with ~30% less memory — but MockServer does not support GraalVM native-image and has no plans to. MockServer loads user-supplied classes at runtime: class callbacks, expectation initializers, and custom matchers. Native-image's closed-world model cannot support runtime class loading, and in testing every feature not explicitly exercised during the build-time metadata-collection step — HTTPS, templates, even logging — failed silently at runtime. The supported lightweight distribution is the jlink binary bundle.

 

The bottom line

Whichever container image you use, the suite-scoped container plus client.reset() pattern described above brings per-test cost down to milliseconds — and that outweighs any runtime-level startup optimisation by a wide margin. For JVM tests that need no network isolation, running MockServer in-process eliminates container overhead entirely.

See also