or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdcontainer-lifecycle.mddocker-client.mddocker-compose.mdimage-building.mdimage-management.mdimage-pull-policies.mdindex.mdjunit-jupiter-integration.mdlifecycle.mdmodules-overview.mdnetwork-configuration.mdoutput-handling.mdstartup-checks.mdutility-classes.mdwait-strategies.md
tile.json

wait-strategies.mddocs/

Wait Strategies

Comprehensive strategies for determining when containers are ready for testing. Wait strategies implement the Strategy pattern to provide flexible approaches for detecting container readiness, from simple port checks to complex HTTP endpoint validation and log message parsing.

Capabilities

Wait Factory Class

Factory class providing convenient methods for creating common wait strategies.

/**
 * Factory for creating common wait strategies.
 */
public class Wait {
    /**
     * Get the default wait strategy.
     *
     * @return default wait strategy (waits for any exposed port)
     */
    public static WaitStrategy defaultWaitStrategy();

    /**
     * Wait for any exposed port to be listening.
     *
     * @return wait strategy for any listening port
     */
    public static HostPortWaitStrategy forListeningPort();

    /**
     * Wait for specific ports to be listening.
     *
     * @param ports the ports to wait for
     * @return wait strategy for specific ports
     */
    public static HostPortWaitStrategy forListeningPorts(int... ports);

    /**
     * Wait for an HTTP endpoint to be available.
     *
     * @param path the HTTP path to check
     * @return HTTP wait strategy
     */
    public static HttpWaitStrategy forHttp(String path);

    /**
     * Wait for an HTTPS endpoint to be available.
     *
     * @param path the HTTPS path to check
     * @return HTTPS wait strategy
     */
    public static HttpWaitStrategy forHttps(String path);

    /**
     * Wait for a specific log message to appear.
     *
     * @param regex regular expression to match in logs
     * @param times number of times the message must appear
     * @return log message wait strategy
     */
    public static LogMessageWaitStrategy forLogMessage(String regex, int times);

    /**
     * Wait for Docker healthcheck to pass.
     *
     * @return Docker healthcheck wait strategy
     */
    public static DockerHealthcheckWaitStrategy forHealthcheck();

    /**
     * Wait for a shell command to succeed.
     *
     * @param command the command to execute
     * @return shell command wait strategy
     */
    public static ShellStrategy forSuccessfulCommand(String command);
}

Usage Examples:

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;

// Wait for default port
GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
    .withExposedPorts(6379)
    .waitingFor(Wait.forListeningPort());

// Wait for HTTP endpoint
GenericContainer<?> webapp = new GenericContainer<>(DockerImageName.parse("nginx:alpine"))
    .withExposedPorts(80)
    .waitingFor(Wait.forHttp("/")
        .forStatusCode(200)
        .withStartupTimeout(Duration.ofSeconds(30)));

// Wait for log message
GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withExposedPorts(8080)
    .waitingFor(Wait.forLogMessage(".*Application started.*", 1));

// Wait for healthcheck
GenericContainer<?> healthy = new GenericContainer<>(DockerImageName.parse("postgres:15"))
    .withExposedPorts(5432)
    .waitingFor(Wait.forHealthcheck());

Decision Guide: Choosing the Right Wait Strategy

Quick Selection Table

Container TypePrimary StrategyFallback/AlternativeReasoning
Database (PostgreSQL, MySQL, MongoDB)Wait.forListeningPort()Wait.forLogMessage(".*ready.*", 1)Port check is fastest; log check more reliable
REST API / Web ServiceWait.forHttp("/health").forStatusCode(200)Wait.forListeningPort()Ensures app is responding, not just port open
Message Queue (Kafka, RabbitMQ)Wait.forLogMessage(".*started.*", 1)Wait.forListeningPort()Startup messages indicate full readiness
Cache (Redis, Memcached)Wait.forListeningPort()-Simple port check sufficient
Elasticsearch / SolrWait.forHttp("/_cluster/health").forStatusCode(200)-Need cluster to be ready, not just node
Custom Image with HEALTHCHECKWait.forHealthcheck()Wait.forHttp() or Wait.forListeningPort()Use built-in healthcheck when available
One-shot/Batch ContainerOneShotStartupCheckStrategyIndefiniteWaitOneShotStartupCheckStrategyFor containers that run and exit
Multi-service (Compose)new WaitAllStrategy()Individual strategies per serviceCoordinate multiple readiness checks

Decision Flowchart Logic

Does the container have a HEALTHCHECK defined?
├─ YES → Use Wait.forHealthcheck()
└─ NO → Does it expose an HTTP endpoint?
    ├─ YES → Does it have a health endpoint?
    │   ├─ YES → Use Wait.forHttp("/health").forStatusCode(200)
    │   └─ NO → Use Wait.forHttp("/").forStatusCode(xxx)
    └─ NO → Does it log a specific startup message?
        ├─ YES → Use Wait.forLogMessage(pattern, times)
        └─ NO → Use Wait.forListeningPort()

Strategy Performance Comparison

StrategyStartup Detection SpeedReliabilityResource UsageUse When
forListeningPort()Fast (100-500ms)MediumLowSimple services, databases
forHttp()Medium (500-2000ms)HighMediumWeb apps, APIs
forLogMessage()Varies (1-5s)HighMedium-High (log streaming)Complex startup sequences
forHealthcheck()Fast (Docker native)HighLowImages with HEALTHCHECK
forSuccessfulCommand()Medium-SlowHighMediumCustom readiness logic
WaitAllStrategySlowest (sum of all)HighestHighestMultiple requirements

Exception Handling

Common Exceptions

// Imports for exception handling
import org.testcontainers.containers.ContainerLaunchException;
import org.rnorth.ducttape.TimeoutException;
import org.rnorth.ducttape.RetryCountExceededException;

// Thrown when container fails to become ready within timeout
org.testcontainers.containers.ContainerLaunchException
    - Caused by: org.rnorth.ducttape.TimeoutException  // Wait strategy timeout
    - Caused by: org.rnorth.ducttape.RetryCountExceededException  // Retry limit exceeded

// Thrown when wait strategy is misconfigured
java.lang.IllegalStateException
    - "Wait strategy target does not expose any ports"
    - "Cannot wait for port when no ports are exposed"

Error Handling Pattern

import org.testcontainers.containers.ContainerLaunchException;
import org.rnorth.ducttape.TimeoutException;

try {
    GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
        .withExposedPorts(8080)
        .waitingFor(Wait.forHttp("/health")
            .forStatusCode(200)
            .withStartupTimeout(Duration.ofSeconds(60)));

    container.start();

} catch (ContainerLaunchException e) {
    // Container failed to start or become ready
    if (e.getCause() instanceof TimeoutException) {
        // Wait strategy timeout - container started but didn't become ready
        System.err.println("Container started but failed readiness check");
        System.err.println("Logs: " + container.getLogs());

        // Common causes:
        // 1. Wrong endpoint path in Wait.forHttp()
        // 2. Incorrect expected status code
        // 3. Application taking longer than timeout
        // 4. Application failing to start properly

    } else {
        // Other startup failure
        System.err.println("Container failed to start: " + e.getMessage());
    }
    throw e;
} catch (IllegalStateException e) {
    // Configuration error - fix the code
    System.err.println("Wait strategy misconfigured: " + e.getMessage());
    throw e;
}

Troubleshooting

Issue: TimeoutException on Wait Strategy

Symptoms:

org.testcontainers.containers.ContainerLaunchException:
  Container startup failed
Caused by: org.rnorth.ducttape.TimeoutException:
  Timeout waiting for condition

Common Causes & Solutions:

  1. Application takes longer to start than timeout

    // Solution: Increase timeout
    .waitingFor(Wait.forHttp("/health")
        .withStartupTimeout(Duration.ofMinutes(2)))  // Increased from default 60s
  2. Wrong HTTP endpoint path

    // Problem: App health check is at /actuator/health, not /health
    .waitingFor(Wait.forHttp("/health"))  // Wrong
    
    // Solution: Use correct path
    .waitingFor(Wait.forHttp("/actuator/health")
        .forStatusCode(200))
  3. Expected wrong status code

    // Problem: Health endpoint returns 503 during startup
    .waitingFor(Wait.forHttp("/health").forStatusCode(200))  // Too strict
    
    // Solution: Accept multiple status codes
    .waitingFor(Wait.forHttp("/health")
        .forStatusCodeMatching(code -> code >= 200 && code < 300))
  4. Application failing to start (check logs)

    try {
        container.start();
    } catch (ContainerLaunchException e) {
        System.err.println("Container logs:");
        System.err.println(container.getLogs());
        throw e;
    }

Issue: IllegalStateException - No Ports Exposed

Symptoms:

java.lang.IllegalStateException:
  Wait strategy target does not expose any ports

Cause: Using port-based wait strategy without exposing ports

Solution:

// Problem
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("app:latest"))
    .waitingFor(Wait.forListeningPort());  // No ports exposed!

// Solution: Expose ports first
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("app:latest"))
    .withExposedPorts(8080)  // Must expose before wait strategy
    .waitingFor(Wait.forListeningPort());

Issue: Log Message Pattern Never Matches

Symptoms: Timeout waiting for log message

Debugging:

// 1. Capture logs to verify actual output
ToStringConsumer logConsumer = new ToStringConsumer();
container.withLogConsumer(logConsumer);
container.start();  // Will timeout

// 2. Print logs to see what actually appears
System.out.println("Actual logs:");
System.out.println(logConsumer.toUtf8String());

// 3. Adjust regex pattern based on actual output

Common Pattern Mistakes:

// Problem: Too specific pattern
.waitingFor(Wait.forLogMessage("Application started successfully on port 8080", 1))

// Solution: Use flexible regex
.waitingFor(Wait.forLogMessage(".*[Aa]pplication.*started.*", 1))

// Problem: Case sensitivity
.waitingFor(Wait.forLogMessage(".*READY.*", 1))  // Won't match "ready"

// Solution: Use case-insensitive pattern
.waitingFor(Wait.forLogMessage("(?i).*ready.*", 1))

Issue: Multiple Strategies Needed (All Must Pass)

Scenario: Container must satisfy multiple conditions

Solution:

import org.testcontainers.containers.wait.strategy.WaitAllStrategy;

WaitAllStrategy allReady = new WaitAllStrategy()
    .withStrategy(Wait.forListeningPort())              // Port must be open
    .withStrategy(Wait.forHttp("/health").forStatusCode(200))  // Health check must pass
    .withStrategy(Wait.forLogMessage(".*Migration complete.*", 1))  // DB migration done
    .withStartupTimeout(Duration.ofMinutes(3));         // Overall timeout

container.waitingFor(allReady);

Performance Optimization

Slow startup times:

// Problem: Overly conservative timeouts
.waitingFor(Wait.forHttp("/health")
    .withStartupTimeout(Duration.ofMinutes(5)))  // Too long for fast app

// Solution: Use appropriate timeout + fast wait strategy
.waitingFor(Wait.forListeningPort()  // Faster than HTTP check
    .withStartupTimeout(Duration.ofSeconds(30)))

// For development: Enable container reuse
container.withReuse(true);  // Requires testcontainers.reuse.enable=true

Reduce false positives:

// Problem: Port opens before app is ready
.waitingFor(Wait.forListeningPort())  // Port open != app ready

// Solution: Use application-level check
.waitingFor(Wait.forHttp("/health")
    .forStatusCode(200)
    .forResponsePredicate(response -> response.contains("\"status\":\"UP\"")))

WaitStrategy Interface

Base interface for all wait strategies.

/**
 * Strategy for determining when a container is ready for testing.
 */
public interface WaitStrategy {
    /**
     * Wait until the target container is ready.
     *
     * @param waitStrategyTarget the container to wait for
     */
    void waitUntilReady(WaitStrategyTarget waitStrategyTarget);

    /**
     * Set the startup timeout for this wait strategy.
     *
     * @param startupTimeout the timeout duration
     * @return this wait strategy for method chaining
     */
    WaitStrategy withStartupTimeout(Duration startupTimeout);
}

WaitStrategyTarget Interface

Interface representing the target container for wait strategies.

/**
 * Target for wait strategies, providing access to container state.
 */
public interface WaitStrategyTarget extends ContainerState {
    /**
     * Get the set of port numbers used for liveness checking.
     * Returns all mapped ports including exposed ports and bound ports.
     *
     * @return set of port numbers for liveness checks
     */
    default Set<Integer> getLivenessCheckPortNumbers();

    // Inherits all methods from ContainerState
    // Provides: isRunning(), getHost(), getMappedPort(), getLogs(), etc.
}

AbstractWaitStrategy

Base class for implementing wait strategies with common functionality.

/**
 * Abstract base class for wait strategies providing common implementation.
 */
public abstract class AbstractWaitStrategy implements WaitStrategy {
    /**
     * The wait strategy target being waited on.
     */
    protected WaitStrategyTarget waitStrategyTarget;

    /**
     * Startup timeout duration (default: 60 seconds).
     */
    protected Duration startupTimeout = Duration.ofSeconds(60);

    /**
     * Wait until the target has started.
     * This public method stores the target and delegates to the protected waitUntilReady().
     *
     * @param waitStrategyTarget the target of the WaitStrategy
     */
    @Override
    public void waitUntilReady(WaitStrategyTarget waitStrategyTarget);

    /**
     * Wait until {@link #waitStrategyTarget} has started.
     * Subclasses must implement this method to define their specific waiting logic.
     */
    protected abstract void waitUntilReady();

    /**
     * Set custom startup timeout.
     *
     * @param startupTimeout timeout duration
     * @return this strategy for method chaining
     */
    @Override
    public WaitStrategy withStartupTimeout(Duration startupTimeout);

    /**
     * Get the ports on which to check if the container is ready.
     *
     * @return the liveness check port numbers from the wait strategy target
     */
    protected Set<Integer> getLivenessCheckPorts();

    /**
     * Get the rate limiter to use for polling.
     *
     * @return the rate limiter
     */
    protected RateLimiter getRateLimiter();

    /**
     * Set a rate limiter to control polling frequency.
     * Allows customization of how frequently the wait strategy checks for readiness.
     *
     * @param rateLimiter rate limiter to control polling rate
     * @return this strategy for method chaining
     */
    public WaitStrategy withRateLimiter(RateLimiter rateLimiter);
}

HostPortWaitStrategy

Wait for one or more ports to be listening on the container.

/**
 * Wait strategy that waits for ports to be listening.
 */
public class HostPortWaitStrategy extends AbstractWaitStrategy {
    /**
     * Wait for multiple ports.
     *
     * @param ports the ports to wait for
     * @return this strategy for method chaining
     */
    public HostPortWaitStrategy forPorts(int... ports);

    /**
     * Internal implementation of waiting for ports to be ready.
     */
    @Override
    protected void waitUntilReady();
}

Usage Example:

GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withExposedPorts(8080, 8081)
    .waitingFor(new HostPortWaitStrategy()
        .forPorts(8080, 8081)
        .withStartupTimeout(Duration.ofSeconds(60)));

HttpWaitStrategy

Wait for an HTTP or HTTPS endpoint to return a successful response.

/**
 * Wait strategy that waits for an HTTP endpoint to be available.
 * Supports HTTP/HTTPS, status code checking, response validation, and authentication.
 */
public class HttpWaitStrategy extends AbstractWaitStrategy {
    /**
     * Set the path to check.
     *
     * @param path the HTTP path (e.g., "/health")
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy forPath(String path);

    /**
     * Set the port to check.
     *
     * @param port the port number
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy forPort(int port);

    /**
     * Expect a specific status code.
     *
     * @param statusCode the expected HTTP status code
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy forStatusCode(int statusCode);

    /**
     * Use a predicate to check the status code.
     *
     * @param predicate predicate that tests the status code
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy forStatusCodeMatching(Predicate<Integer> predicate);

    /**
     * Use a predicate to check the response body.
     *
     * @param predicate predicate that tests the response body
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy forResponsePredicate(Predicate<String> predicate);

    /**
     * Set Basic authentication credentials.
     *
     * @param username the username
     * @param password the password
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy withBasicCredentials(String username, String password);

    /**
     * Set the HTTP method to use.
     *
     * @param method the HTTP method (GET, POST, etc.)
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy withMethod(String method);

    /**
     * Add an HTTP header.
     *
     * @param name header name
     * @param value header value
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy withHeader(String name, String value);

    /**
     * Add multiple HTTP headers.
     *
     * @param headers map of header names to values
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy withHeaders(Map<String, String> headers);

    /**
     * Set the read timeout for HTTP requests.
     *
     * @param timeout the read timeout duration
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy withReadTimeout(Duration timeout);

    /**
     * Use TLS/HTTPS.
     *
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy usingTls();

    /**
     * Allow insecure TLS connections (skip certificate validation).
     *
     * @return this strategy for method chaining
     */
    public HttpWaitStrategy allowInsecure();
}

Usage Examples:

// Simple HTTP check
HttpWaitStrategy simple = Wait.forHttp("/health")
    .forStatusCode(200);

// HTTPS with custom headers
HttpWaitStrategy secure = Wait.forHttps("/api/status")
    .forPort(8443)
    .withHeader("X-API-Key", "secret")
    .allowInsecure()
    .withStartupTimeout(Duration.ofMinutes(2));

// HTTP with authentication
HttpWaitStrategy auth = Wait.forHttp("/admin")
    .withBasicCredentials("admin", "password")
    .forStatusCode(200);

// HTTP with response validation
HttpWaitStrategy validated = Wait.forHttp("/health")
    .forStatusCodeMatching(code -> code >= 200 && code < 300)
    .forResponsePredicate(response -> response.contains("\"status\":\"UP\""));

// Custom HTTP method
HttpWaitStrategy post = Wait.forHttp("/ready")
    .withMethod("POST")
    .withHeader("Content-Type", "application/json");

// Using the strategy
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("webapp:latest"))
    .withExposedPorts(8080)
    .waitingFor(simple);

LogMessageWaitStrategy

Wait for a specific log message or pattern to appear in container logs.

/**
 * Wait strategy that waits for a log message matching a regex pattern.
 */
public class LogMessageWaitStrategy extends AbstractWaitStrategy {
    /**
     * Set the regex pattern to match.
     *
     * @param regex regular expression pattern
     * @return this strategy for method chaining
     */
    public LogMessageWaitStrategy withRegEx(String regex);

    /**
     * Set how many times the pattern must appear.
     *
     * @param times number of occurrences required
     * @return this strategy for method chaining
     */
    public LogMessageWaitStrategy withTimes(int times);

    /**
     * Internal implementation of waiting for log messages.
     */
    @Override
    protected void waitUntilReady();
}

Usage Examples:

// Wait for single message
LogMessageWaitStrategy single = Wait.forLogMessage(".*Application started.*", 1);

// Wait for multiple occurrences
LogMessageWaitStrategy multiple = new LogMessageWaitStrategy()
    .withRegEx(".*Worker \\d+ ready.*")
    .withTimes(3)  // Wait for 3 workers to be ready
    .withStartupTimeout(Duration.ofSeconds(90));

// Complex pattern
LogMessageWaitStrategy pattern = Wait.forLogMessage(".*Server listening on port \\d+.*", 1);

// Using the strategy
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .waitingFor(single);

DockerHealthcheckWaitStrategy

Wait for the container's Docker healthcheck to pass.

/**
 * Wait strategy that waits for the Docker healthcheck to become healthy.
 * Requires the container image to define a HEALTHCHECK instruction.
 */
public class DockerHealthcheckWaitStrategy extends AbstractWaitStrategy {
    /**
     * Internal implementation of waiting for healthcheck.
     */
    @Override
    protected void waitUntilReady();
}

Usage Example:

// Container image must define HEALTHCHECK in Dockerfile
// Example Dockerfile:
// HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
//   CMD pg_isready -U postgres || exit 1

GenericContainer<?> postgres = new GenericContainer<>(DockerImageName.parse("postgres:15"))
    .withExposedPorts(5432)
    .waitingFor(Wait.forHealthcheck()
        .withStartupTimeout(Duration.ofMinutes(1)));

ShellStrategy

Wait for a shell command to execute successfully inside the container.

/**
 * Wait strategy that executes a shell command and waits for it to succeed.
 */
public class ShellStrategy extends AbstractWaitStrategy {
    /**
     * Set the command to execute.
     *
     * @param command the shell command
     * @return this strategy for method chaining
     */
    public ShellStrategy withCommand(String command);

    /**
     * Internal implementation of waiting for shell command to succeed.
     */
    @Override
    protected void waitUntilReady();
}

Usage Examples:

// Wait for file to exist
ShellStrategy fileExists = Wait.forSuccessfulCommand("test -f /app/ready.txt");

// Wait for database to be ready
ShellStrategy dbReady = new ShellStrategy()
    .withCommand("pg_isready -U postgres")
    .withStartupTimeout(Duration.ofSeconds(60));

// Wait for service to respond
ShellStrategy serviceReady = Wait.forSuccessfulCommand(
    "curl -f http://localhost:8080/health"
);

// Using the strategy
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .waitingFor(fileExists);

WaitAllStrategy

Composite wait strategy that waits for multiple strategies to be satisfied. Provides flexible timeout modes to handle complex readiness requirements.

/**
 * Wait strategy that combines multiple strategies, waiting for all of them to succeed.
 * Useful for containers that have multiple readiness conditions.
 *
 * Note: WaitAllStrategy implements WaitStrategy directly (not via AbstractWaitStrategy).
 * Therefore, it does not inherit withRateLimiter() and other AbstractWaitStrategy methods.
 */
public class WaitAllStrategy implements WaitStrategy {
    /**
     * Default constructor using WITH_OUTER_TIMEOUT mode.
     */
    public WaitAllStrategy();

    /**
     * Constructor with specific mode.
     *
     * @param mode the timeout mode
     */
    public WaitAllStrategy(Mode mode);

    /**
     * Add a wait strategy to the list.
     *
     * @param strategy the strategy to add
     * @return this strategy for method chaining
     */
    public WaitAllStrategy withStrategy(WaitStrategy strategy);

    /**
     * Set the overall timeout for all strategies.
     *
     * @param startupTimeout the timeout duration
     * @return this strategy for method chaining
     * @throws IllegalStateException if mode is WITH_INDIVIDUAL_TIMEOUTS_ONLY
     */
    @Override
    public WaitAllStrategy withStartupTimeout(Duration startupTimeout);

    @Override
    public void waitUntilReady(WaitStrategyTarget waitStrategyTarget);

    /**
     * Timeout behavior modes for WaitAllStrategy.
     */
    public enum Mode {
        /**
         * Default mode: The timeout is applied to each individual strategy.
         * All strategies must complete within the outer timeout.
         */
        WITH_OUTER_TIMEOUT,

        /**
         * The outer timeout is disabled and each strategy uses its own timeout.
         * The withStartupTimeout method is disabled in this mode.
         */
        WITH_INDIVIDUAL_TIMEOUTS_ONLY,

        /**
         * Inner strategies use their individual timeouts, but the outer strategy
         * will terminate them if the maximum outer timeout is reached.
         */
        WITH_MAXIMUM_OUTER_TIMEOUT
    }
}

Usage Examples:

// Wait for multiple conditions with default mode
WaitAllStrategy waitStrategy = new WaitAllStrategy()
    .withStrategy(Wait.forListeningPort())
    .withStrategy(Wait.forHttp("/health").forStatusCode(200))
    .withStrategy(Wait.forLogMessage(".*Started successfully.*", 1))
    .withStartupTimeout(Duration.ofSeconds(60));

GenericContainer<?> webapp = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withExposedPorts(8080)
    .waitingFor(waitStrategy);

// Wait for multiple conditions with individual timeouts
WaitAllStrategy individualTimeouts = new WaitAllStrategy(WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY)
    .withStrategy(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
    .withStrategy(Wait.forHttp("/health").forStatusCode(200).withStartupTimeout(Duration.ofSeconds(60)))
    .withStrategy(Wait.forLogMessage(".*Database migration complete.*", 1).withStartupTimeout(Duration.ofMinutes(2)));

GenericContainer<?> complexApp = new GenericContainer<>(DockerImageName.parse("complex:latest"))
    .withExposedPorts(8080)
    .waitingFor(individualTimeouts);

// Wait with maximum outer timeout (individual timeouts respected, but overall limit enforced)
WaitAllStrategy maxOuterTimeout = new WaitAllStrategy(WaitAllStrategy.Mode.WITH_MAXIMUM_OUTER_TIMEOUT)
    .withStrategy(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
    .withStrategy(Wait.forHttp("/health").forStatusCode(200).withStartupTimeout(Duration.ofSeconds(60)))
    .withStrategy(Wait.forLogMessage(".*Ready.*", 1).withStartupTimeout(Duration.ofSeconds(90)))
    .withStartupTimeout(Duration.ofMinutes(2)); // Overall maximum timeout

GenericContainer<?> timedApp = new GenericContainer<>(DockerImageName.parse("timed:latest"))
    .withExposedPorts(8080)
    .waitingFor(maxOuterTimeout);

Startup Check Strategies

Strategies for determining if a container has started correctly, independent of wait strategies.

StartupCheckStrategy

Base class for startup check strategies.

/**
 * Strategy for checking if a container has started correctly.
 */
public abstract class StartupCheckStrategy {
    /**
     * Startup status values.
     */
    public enum StartupStatus {
        /** Status not yet determined */
        NOT_YET_KNOWN,
        /** Container started successfully */
        SUCCESSFUL,
        /** Container failed to start */
        FAILED
    }

    /**
     * Wait until the container startup is successful or failed.
     *
     * @param container the container
     * @return true if startup successful, false if failed
     * @deprecated internal API
     */
    @Deprecated
    public boolean waitUntilStartupSuccessful(GenericContainer<?> container);

    /**
     * Wait until the container startup is successful or failed.
     *
     * @param dockerClient the Docker client
     * @param containerId the container ID
     * @return true if startup successful, false if failed
     */
    public boolean waitUntilStartupSuccessful(DockerClient dockerClient, String containerId);

    /**
     * Check the current startup state without blocking.
     *
     * @param dockerClient the Docker client
     * @param containerId the container ID
     * @return the current startup status
     */
    public abstract StartupStatus checkStartupState(DockerClient dockerClient, String containerId);

    /**
     * Set the timeout for startup checks.
     *
     * @param timeout the timeout duration
     * @return this strategy (with generic type for proper subclass chaining)
     */
    public <SELF extends StartupCheckStrategy> SELF withTimeout(Duration timeout);
}

IsRunningStartupCheckStrategy

Check if the container is running (default strategy).

/**
 * Startup check strategy that verifies the container is running.
 * This is the default strategy for most containers.
 */
public class IsRunningStartupCheckStrategy extends StartupCheckStrategy {
    @Override
    public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}

OneShotStartupCheckStrategy

For containers that run a single command and exit.

/**
 * Startup check strategy for one-shot containers that run and exit.
 * Considers the container started when it has finished (exited).
 */
public class OneShotStartupCheckStrategy extends StartupCheckStrategy {
    @Override
    public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}

Usage Example:

// Container that runs a command and exits
GenericContainer<?> oneShot = new GenericContainer<>(DockerImageName.parse("alpine:latest"))
    .withCommand("sh", "-c", "echo 'Processing...' && sleep 5 && echo 'Done'")
    .withStartupCheckStrategy(new OneShotStartupCheckStrategy());

oneShot.start();
// Wait for container to exit
String logs = oneShot.getLogs();

IndefiniteWaitOneShotStartupCheckStrategy

For one-shot containers that may take a long time.

/**
 * Startup check strategy for one-shot containers that waits indefinitely.
 * Use when the container execution time is unpredictable.
 */
public class IndefiniteWaitOneShotStartupCheckStrategy extends StartupCheckStrategy {
    @Override
    public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}

MinimumDurationRunningStartupCheckStrategy

Ensures the container runs for a minimum duration before being considered started.

/**
 * Startup check strategy that ensures the container stays running
 * for a minimum duration. Useful for detecting containers that start
 * but crash immediately.
 */
public class MinimumDurationRunningStartupCheckStrategy extends StartupCheckStrategy {
    /**
     * Constructor that sets the minimum duration the container must run.
     *
     * @param minimumRunningDuration the minimum duration
     */
    public MinimumDurationRunningStartupCheckStrategy(@NotNull Duration minimumRunningDuration);

    @Override
    public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}

Usage Example:

// Ensure container runs for at least 10 seconds without crashing
GenericContainer<?> stable = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
    .withStartupCheckStrategy(
        new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(10))
    );

Complete Usage Example

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;

public class CompleteWaitStrategyExample {

    public void testWithVariousWaitStrategies() {
        Network network = Network.newNetwork();

        // Database with healthcheck
        GenericContainer<?> database = new GenericContainer<>(DockerImageName.parse("postgres:15"))
            .withNetwork(network)
            .withNetworkAliases("db")
            .withExposedPorts(5432)
            .withEnv("POSTGRES_PASSWORD", "secret")
            .waitingFor(Wait.forHealthcheck()
                .withStartupTimeout(Duration.ofMinutes(1)));

        // Cache with port check
        GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
            .withNetwork(network)
            .withNetworkAliases("cache")
            .withExposedPorts(6379)
            .waitingFor(Wait.forListeningPort());

        // Web application with multiple checks
        WaitAllStrategy appWaitStrategy = new WaitAllStrategy()
            .withStrategy(Wait.forHttp("/health")
                .forStatusCode(200)
                .forResponsePredicate(r -> r.contains("\"status\":\"UP\"")))
            .withStrategy(Wait.forLogMessage(".*Application ready to serve requests.*", 1))
            .withStartupTimeout(Duration.ofMinutes(2));

        GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("myapp:latest"))
            .withNetwork(network)
            .withExposedPorts(8080)
            .withEnv("DATABASE_URL", "postgresql://db:5432/test")
            .withEnv("REDIS_URL", "redis://cache:6379")
            .dependsOn(database, redis)
            .waitingFor(appWaitStrategy);

        // Start all containers
        app.start();

        // Containers are now ready for testing
        String appUrl = String.format("http://%s:%d",
            app.getHost(), app.getMappedPort(8080));
    }
}