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:

  • Programmatic start — add mockserver-netty as a test dependency and call ClientAndServer.startClientAndServer(0). MockServer starts on a random free port inside the already-warm JVM and is ready in tens of milliseconds.
  • JUnit 5 extension — add mockserver-junit-jupiter as a test dependency and annotate the test class with @MockServerSettings (which already includes @ExtendWith(MockServerExtension.class) internally). The extension starts MockServer before any tests run and stops it after all tests complete. See Running MockServer via JUnit 5 Test Extension for full usage and options.

Maven

<!-- programmatic start -->
<dependency>
    <groupId>org.mock-server</groupId>
    <artifactId>mockserver-netty</artifactId>
    <version>7.4.0</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>7.4.0</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.4.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.4.0 .

Why it is not the default image:

  • The AOT cache is CPU-architecture and JDK-build specific — a multi-arch build bakes a separate cache per platform, which adds complexity.
  • There is no distroless Java 25 base image yet; this variant copies a jlink-trimmed JDK 25 runtime onto gcr.io/distroless/java-base-debian12:nonroot.
  • The AOT cache format may change between JDK releases while the feature is still maturing.
  • This variant uses the JDK TLS provider rather than netty-tcnative, which may reduce TLS handshake throughput under high load.

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

  • Testcontainers — official MockServer Testcontainers modules for Java, .NET, Node.js, Python, Rust and Go
  • Start-up Time — the full breakdown of container startup times, image variants, and the built-in warmup
  • Running MockServer — all deployment options including Docker, jar, JUnit, and Testcontainers
  • MockServer clients — typed client libraries for every supported language