Mercure 1.0 alpha is available. Check out the new docs
Sponsored by Les-Tilleuls.coop
DocumentationSpecificationCloudDemos
Contribute!

Load testing

The Mercure repository ships a Gatling-based load test. Use it to measure your own infrastructure before users do.

For reference, a public benchmark by Glory4Gamers reached 40,000 concurrent connections on a single EC2 t3.micro running the open-source hub. Your numbers will vary with kernel limits, NIC, and publish rate. Don't take 40k as a ceiling; take it as "one node holds a lot."

Run the Mercure Gatling load test

# Run the Mercure Gatling Load Test
git clone https://github.com/dunglas/mercure
cd mercure/gatling
./mvnw gatling:test

Without configuration, the test hits a local hub on https://localhost. To target a real hub, set HUB_URL and a publisher JWT.

Mercure load test configuration

All variables are optional.

VariableDescription
HUB_URLURL of the hub to test.
JWTPublisher JWT.
SUBSCRIBER_JWTSubscriber JWT. Falls back to JWT when private updates are tested.
INITIAL_SUBSCRIBERSConcurrent subscribers connected at the start.
SUBSCRIBERS_RATE_FROM / SUBSCRIBERS_RATE_TORange for additional subscriber connection rate (per second).
PUBLISHERS_RATE_FROM / PUBLISHERS_RATE_TORange for publication rate (per second).
INJECTION_DURATIONHow long the publisher load runs.
CONNECTION_DURATIONHow long subscribers stay connected.
RANDOM_CONNECTION_DURATIONRandomize subscriber lifetime up to CONNECTION_DURATION.
PRIVATE_UPDATESIf set, send private updates with random topics instead of public updates with one topic.

A useful starting recipe (build up to your expected traffic):

# Mercure Load Test Configuration
HUB_URL=https://hub.example.com/.well-known/mercure \
JWT=<publisher JWT> \
INITIAL_SUBSCRIBERS=1000 \
SUBSCRIBERS_RATE_FROM=50 \
SUBSCRIBERS_RATE_TO=200 \
PUBLISHERS_RATE_FROM=10 \
PUBLISHERS_RATE_TO=100 \
INJECTION_DURATION=300 \
CONNECTION_DURATION=600 \
./mvnw gatling:test

What to measure during a Mercure load test

While the test runs, watch:

  • mercure_subscribers_connected: should track the configured ramp.

  • CPU and memory of the hub process: establishes the per-subscriber cost on your hardware.

  • Open file descriptors (ls /proc/<pid>/fd | wc -l): every subscriber takes one. Compare to your ulimit -n.

  • Publish latency: Caddy request duration histogram on POST /.well-known/mercure.

  • Subscriber receive latency: built into the Gatling report.

What changes the numbers

Connections themselves are cheap. What scales the cost:

  • Publish rate x number of matching subscribers per topic. A 1-publish-per-second feed to 100k subscribers is far heavier than 1k publishes per second to 100 subscribers each.

  • Dispatch timeout. Slow subscribers blocking dispatch eat goroutines until dispatch_timeout cuts them off.

  • Matcher complexity. Exact matchers are O(1); URL Pattern matchers cost time per evaluation. Use topic_matcher_cache for repeated patterns.

  • History writes. BoltDB syncs to disk; write throughput is bounded by your storage. The Postgres transport is faster on bursty writes; Redis is the fastest.

Common Mercure hub bottlenecks

SymptomProbable cause
accept: too many open files in logsulimit -n too low. Set 100000 or higher on the host.
CPU spent in matcher evaluationURL Pattern matchers; raise topic_matcher_cache.
Dispatch latency rising under loadSlow subscribers; lower dispatch_timeout to bound the impact.
Memory growth that doesn't plateauGoroutine leak; capture a pprof heap and goroutine snapshot (Debugging) and file an issue.
Test plateaus before the box doesBackpressure from the hub's listener; check net.core.somaxconn and net.ipv4.tcp_max_syn_backlog on Linux.

File descriptor limits for the Mercure hub

The single most common limit. On Linux:

# Per process (the running hub)
prlimit --pid $(pgrep mercure)

# Set globally for the next process you start
ulimit -n 100000

# Persist via systemd
# /etc/systemd/system/mercure.service.d/override.conf
[Service]
LimitNOFILE=100000

In Docker:

# File Descriptor Limits for the Mercure Hub
services:
  mercure:
    ulimits:
      nofile:
        soft: 100000
        hard: 100000

In Kubernetes, the host's limit applies to the container by default. If the host is set to 1024, that's your ceiling. Bump it on the node.

Mercure conformance vs. Load testing

Load tests measure throughput. Conformance tests check correctness. Run both:

When to scale beyond one node

Symptoms that mean a single node won't get you any further:

  • CPU pinned at 100% during normal traffic, no headroom for spikes.

  • Network bandwidth saturated by fan-out (publish x subscribers per topic exceeds what your NIC can deliver).

  • You need geographic redundancy, not just headroom.

At that point: High availability.

Next steps for Mercure load testing