Gatling Cloud and Distributed Load Testing: Scaling Beyond One Machine

Gatling Cloud and Distributed Load Testing: Scaling Beyond One Machine

A single Gatling machine can generate impressive load—10,000 to 50,000 concurrent users depending on hardware and workload. But some systems need more. Global applications require load from multiple geographic regions. Financial platforms need to simulate six-figure concurrent user counts. Distributed testing is where you go when one machine isn't enough.

This guide covers the full spectrum: open-source manual clustering, containerized deployments, cloud infrastructure patterns, and Gatling Enterprise for teams that want managed distribution.

When You Actually Need Distributed Testing

Before adding infrastructure complexity, assess whether you really need it.

Signs you need distributed testing:

  • Single machine CPU hits 80%+ during test (you're bottlenecking on load generation, not the target)
  • You need to simulate geographic distribution (users in US, EU, APAC simultaneously)
  • Target system requires more requests/second than one machine can generate
  • Network bandwidth from a single machine approaches saturation

Signs you don't need distributed testing:

  • Response times are high but machine CPU/network is idle (the target is the bottleneck, which is what you're measuring)
  • Gatling node memory is exhausted (try tuning JVM heap first)
  • You just want "more users" without diagnosing what's actually limiting you

Distributed testing multiplies infrastructure cost and complexity. Validate the need before committing.

Open-Source Distributed Testing

Gatling's open-source distribution doesn't have a built-in distributed mode. Distribution requires manual coordination: multiple Gatling instances run the same simulation independently, and results are aggregated after the fact.

The Basic Approach

  1. Stand up N identical machines (load injectors)
  2. Deploy the same simulation to each
  3. Configure each to use 1/N of the total user count
  4. Run all instances simultaneously
  5. Aggregate the results

For a target of 10,000 users across 5 machines:

// Each machine runs with users=2000
val users = System.getProperty("users", "2000").toInt

setUp(
  scn.inject(rampUsers(users).during(300.seconds))
).protocols(httpProtocol)

Coordinating with a Shell Script

#!/bin/bash
# distribute.sh - launch simulation on multiple AWS instances

INSTANCES=("10.0.1.10" "10.0.1.11" "10.0.1.12" "10.0.1.13" "10.0.1.14")
SIMULATION="simulations.ApiLoadTest"
USERS_PER_INSTANCE=2000
DURATION=300
RUN_ID=$(date +%Y%m%d-%H%M%S)

echo "Starting distributed load test: $RUN_ID"
echo "Instances: ${#INSTANCES[@]}, Users per instance: $USERS_PER_INSTANCE"

# Deploy simulation to all instances
for INSTANCE in "${INSTANCES[@]}"; do
  echo "Deploying to $INSTANCE..."
  rsync -az --delete \
    ./load-tests/ \
    "ec2-user@$INSTANCE:/opt/load-tests/"
done

# Start simultaneously (background each)
PIDS=()
for INSTANCE in "${INSTANCES[@]}"; do
  ssh "ec2-user@$INSTANCE" \
    "cd /opt/load-tests && \
     mvn gatling:test \
       -Dgatling.simulationClass=$SIMULATION \
       -Dusers=$USERS_PER_INSTANCE \
       -Dduration=$DURATION \
       -Dtest.run.id=$RUN_ID \
       > /tmp/gatling-$RUN_ID.log 2>&1" &
  PIDS+=($!)
  echo "Started on $INSTANCE (PID: $!)"
done

# Wait for all to complete
echo "Waiting for all instances to complete..."
for PID in "${PIDS[@]}"; do
  wait $PID
  echo "Instance PID $PID completed with status $?"
done

# Collect results
mkdir -p ./results/$RUN_ID
for INSTANCE in "${INSTANCES[@]}"; do
  rsync -az \
    "ec2-user@$INSTANCE:/opt/load-tests/target/gatling/" \
    "./results/$RUN_ID/$INSTANCE/"
  echo "Collected results from $INSTANCE"
done

echo "All results collected in ./results/$RUN_ID"
echo "Aggregate manually or use a results merger tool"

Aggregating Results

Open-source Gatling doesn't aggregate distributed results automatically. Options:

Merge simulation.log files: Gatling's raw output is in simulation.log format. Concatenate logs from all nodes (adjusting timestamps to ensure ordering), then generate a report:

# Collect all simulation.log files
cat \
  results/RUN_ID/10.0.1.10/ApiLoadTest-*/simulation.log \
  results/RUN_ID/10.0.1.11/ApiLoadTest-*/simulation.log \
  results/RUN_ID/10.0.1.12/ApiLoadTest-*/simulation.log \
  > results/RUN_ID/combined/simulation.log

# Generate report from combined log
$GATLING_HOME/bin/gatling.sh \
  -ro results/RUN_ID/combined/ \
  -rf results/RUN_ID/report/

Per-node reports with manual comparison: Generate individual reports per node, compare numbers manually. Less clean but avoids log manipulation.

Grafana dashboard: Stream all nodes to InfluxDB via Graphite protocol. Grafana aggregates automatically. This is the most operationally clean approach for ongoing distributed testing.

Docker-Based Distribution

Containerizing Gatling simplifies deployment and ensures consistent environments.

Dockerfile

FROM eclipse-temurin:17-jdk-alpine

WORKDIR /gatling

# Install Gatling bundle
ARG GATLING_VERSION=3.11.5
RUN apk add --no-cache wget unzip && \
    wget -q https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/${GATLING_VERSION}/gatling-charts-highcharts-bundle-${GATLING_VERSION}-bundle.zip && \
    unzip -q gatling-charts-highcharts-bundle-${GATLING_VERSION}-bundle.zip && \
    mv gatling-charts-highcharts-bundle-${GATLING_VERSION}/* . && \
    rm -rf gatling-charts-highcharts-bundle-${GATLING_VERSION}-bundle.zip && \
    apk del wget unzip

# Copy simulations
COPY simulations/ /gatling/simulations/
COPY user-files/ /gatling/user-files/

ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC"

ENTRYPOINT ["/gatling/bin/gatling.sh"]
CMD ["-s", "simulations.ApiLoadTest"]

Build and push

docker build -t my-registry/gatling-load:latest .
docker push my-registry/gatling-load:latest

Docker Compose for local distributed testing

# docker-compose.yml
version: '3.8'

services:
  injector-1:
    image: my-registry/gatling-load:latest
    environment:
      - JAVA_OPTS=-Xmx2g
    command: ["-s", "simulations.ApiLoadTest"]
    env_file:
      - .env.load-test
    volumes:
      - ./results/node-1:/gatling/results
    extra_hosts:
      - "host.docker.internal:host-gateway"

  injector-2:
    image: my-registry/gatling-load:latest
    environment:
      - JAVA_OPTS=-Xmx2g
    command: ["-s", "simulations.ApiLoadTest"]
    env_file:
      - .env.load-test
    volumes:
      - ./results/node-2:/gatling/results
    extra_hosts:
      - "host.docker.internal:host-gateway"

  injector-3:
    image: my-registry/gatling-load:latest
    environment:
      - JAVA_OPTS=-Xmx2g
    command: ["-s", "simulations.ApiLoadTest"]
    env_file:
      - .env.load-test
    volumes:
      - ./results/node-3:/gatling/results
    extra_hosts:
      - "host.docker.internal:host-gateway"

.env.load-test:

TARGET_URL=https://api.staging.example.com
USERS=2000
DURATION=300
P95_THRESHOLD=500

Run all nodes simultaneously:

docker compose up --exit-code-from injector-1

Kubernetes Deployment

For production-scale distributed testing on Kubernetes:

Job manifest

apiVersion: batch/v1
kind: Job
metadata:
  name: gatling-load-test
  labels:
    app: gatling
    run-id: "20260526-001"
spec:
  completions: 5      # Total number of injector pods
  parallelism: 5      # All run simultaneously
  backoffLimit: 0     # Don't retry failed pods
  template:
    metadata:
      labels:
        app: gatling
    spec:
      restartPolicy: Never
      containers:
        - name: gatling
          image: my-registry/gatling-load:latest
          args: ["-s", "simulations.ApiLoadTest"]
          env:
            - name: TARGET_URL
              valueFrom:
                secretKeyRef:
                  name: load-test-secrets
                  key: target-url
            - name: USERS
              value: "2000"
            - name: DURATION
              value: "300"
          resources:
            requests:
              memory: "1Gi"
              cpu: "1"
            limits:
              memory: "3Gi"
              cpu: "2"
          volumeMounts:
            - name: results
              mountPath: /gatling/results
      volumes:
        - name: results
          emptyDir: {}
      # Spread across nodes for realistic network load
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values: [gatling]
                topologyKey: kubernetes.io/hostname

Deploy and monitor:

kubectl apply -f gatling-job.yaml

# Watch progress
kubectl get pods -l app=gatling -w

# Get logs from all pods
kubectl logs -l app=gatling --prefix=true

# Wait for completion
kubectl wait --for=condition=complete job/gatling-load-test --timeout=1800s

Collecting Results from Kubernetes

#!/bin/bash
# Collect results from all completed pods

mkdir -p distributed-results

for POD in $(kubectl get pods -l app=gatling -o name | grep Completed); do
  POD_NAME=${POD#pod/}
  mkdir -p "distributed-results/$POD_NAME"
  kubectl cp "$POD_NAME:/gatling/results/." "distributed-results/$POD_NAME/"
  echo "Collected from $POD_NAME"
done

Geographic Distribution

Testing from multiple cloud regions requires injectors deployed in each target region.

Multi-Region AWS Pattern

# Terraform example
resource "aws_instance" "gatling_injector" {
  for_each = {
    "us-east-1" = "ami-0c55b159cbfafe1f0"
    "eu-west-1"  = "ami-0a8e758f5e873d1c1"
    "ap-southeast-1" = "ami-0f02b24005e4aec36"
  }

  provider      = aws.${each.key}
  ami           = each.value
  instance_type = "c5.2xlarge"

  tags = {
    Name   = "gatling-injector-${each.key}"
    Region = each.key
  }

  user_data = base64encode(templatefile("${path.module}/setup.sh", {
    region = each.key
  }))
}

Configure simulations to identify their origin:

val httpProtocol = http
  .baseUrl(targetUrl)
  .header("X-Load-Test-Region", System.getProperty("aws.region", "unknown"))
  .header("X-Load-Test-Node", System.getProperty("node.id", "local"))

This allows filtering results by region in post-analysis.

Gatling Enterprise

Gatling Enterprise (formerly Gatling FrontLine) is the commercial solution for teams that want managed distributed load generation without the infrastructure complexity.

What It Provides

  • Web-based UI: Run simulations, view results, and manage tests from a dashboard
  • Built-in distribution: Specify "1000 injectors in us-east-1, 500 in eu-west-1" without manual setup
  • Automatic result aggregation: All nodes' results merged into one report automatically
  • Cloud or on-premises: Deploy injectors on AWS, GCP, Azure, or your own infrastructure
  • Simulation repositories: Connect to Git repositories and run simulations directly
  • Real-time metrics: Live dashboard during test execution
  • Trend analysis: Compare runs over time, track performance regressions

Deployment Models

Gatling Enterprise Cloud: Fully managed SaaS. Upload your simulation, configure targets and injection profile, run.

Gatling Enterprise Self-Hosted: Deploy the control plane on your infrastructure. Injectors spin up on your cloud account.

Code Changes Required

Simulations written for open-source Gatling work with Gatling Enterprise without modification. You add the enterprise plugin to your build:

<!-- pom.xml -->
<plugin>
  <groupId>io.gatling.enterprise</groupId>
  <artifactId>gatling-enterprise-maven-plugin</artifactId>
  <version>1.9.0</version>
  <configuration>
    <simulationId>${gatling.enterprise.simulationId}</simulationId>
  </configuration>
</plugin>
# Deploy to Enterprise and run
mvn gatling-enterprise:start \
  -Dgatling.enterprise.apiToken=$GATLING_ENTERPRISE_TOKEN \
  -Dgatling.enterprise.simulationId=$SIMULATION_ID

Cost Considerations

Gatling Enterprise pricing is usage-based on virtual user-hours. For teams running occasional load tests, the cost of managed infrastructure often compares favorably to the engineering time spent maintaining DIY distributed setups.

The calculation: if your team spends 2 days/month maintaining distributed testing infrastructure at $150/hour loaded engineering cost, that's $2,400/month in engineering overhead. Gatling Enterprise typically costs less for comparable virtual user-hours.

Cloud-Native Load Generation: Alternatives

While Gatling is the focus here, context helps when deciding on architecture:

AWS Distributed Load Testing: AWS CloudFormation solution using ECS containers. Works well if you're already AWS-native.

k6: Grafana's load testing tool with built-in cloud execution (k6 cloud). Different scripting model (JavaScript), but strong cloud integration.

Azure Load Testing: Azure-native, supports JMeter tests. Good choice for Azure-first organizations.

Gatling's advantage remains its performance efficiency—fewer VMs needed for equivalent load—which translates to lower cloud costs at scale.

Practical Scaling Recommendations

Start with profiling, not speculation: Before buying cloud instances, run your simulation on one machine with htop monitoring. What bottlenecks first—CPU, memory, or network?

Use spot/preemptible instances: Load tests run for minutes to hours, not days. Spot instances (AWS) or preemptible VMs (GCP) cut injector costs by 60-90%.

Right-size your instances: Compute-optimized instances (c5.2xlarge on AWS, c2 on GCP) suit Gatling well. More CPU and network, less memory needed.

Keep injectors close to the target: Injectors and target system in the same VPC/region eliminate network latency from the measurement. Unless you're specifically testing global latency, keep them co-located.

Monitor injector health during tests: A CPU-saturated injector produces inaccurate results—it's not the target that's slow, it's the injector. Add CloudWatch or Datadog monitoring to injector nodes.

Wrapping Up

Distributed load testing with Gatling ranges from DIY shell scripting across EC2 instances to fully managed Gatling Enterprise deployments. The right choice depends on your team's infrastructure expertise, testing frequency, and budget.

For most teams starting out: a single well-provisioned machine handles more than expected. When you genuinely outgrow it, containerized Kubernetes jobs offer the best balance of flexibility and repeatability. Gatling Enterprise makes sense when the operational overhead of managing distribution yourself costs more than the subscription.

Whatever approach you choose, the simulation code is the same. That portability is one of Gatling's core strengths.

Read more

Start now free