A protocol-agnostic load testing framework in Elixir.
Typhoon load tests anything you can call from Elixir — HTTP services, custom TCP protocols, proprietary binary wire formats, message brokers, databases. You supply the client that speaks your protocol; Typhoon drives the load around it — spawning concurrent virtual users, ramping and shaping load over time, replaying scenarios, and capturing per-step latency and throughput metrics.
- Installation
- Features
- Usage
- Contributing
- Reference — components, config options, sessions and step output, load profiles, datasets, cross-module scenarios, and running a test
Typhoon is not yet published to Hex, so add it as a Git dependency in your
mix.exs:
def deps do
[
{:typhoon, git: "https://github.com/rahultumpala/typhoon.git"}
]
endPin to a specific tag, branch, or commit for reproducible builds:
def deps do
[
# a tag
{:typhoon, git: "https://github.com/rahultumpala/typhoon.git", tag: "v0.1.0"},
# or a branch
{:typhoon, git: "https://github.com/rahultumpala/typhoon.git", branch: "main"},
# or a commit
{:typhoon, git: "https://github.com/rahultumpala/typhoon.git", ref: "a1b2c3d"}
]
endThen fetch it:
mix deps.getA load test is one or more scenarios, each replayed concurrently by a pool of virtual users.
Implemented:
- Virtual users with ramp up over a configurable duration
- Test duration: users replay until a wall-clock deadline
- Load profiles: multi-phase concurrency shaping (ramp up / hold / spike / ramp down, in any order and to any level)
- Scenarios of ordered, isolated steps with delays between them
- Per-user sessions, seeded once and threaded through every step
- Previous step's output passed to the next step
before_scenario/after_scenariohooks with session access- Replay a single step N times
- Replay a scenario N times or for a fixed duration (mutually exclusive)
- Per-step and per-scenario metrics with latency percentiles, throughput (steps/sec), error rate, and an end-of-run summary
- Datasets (fixed and circular) feeding one entry per replay
- Scenarios composed across modules, with per-scenario config overrides
- Aggregation of every replay's final step output for the teardown hook
Planned (see road-map.md):
- Run-time config overrides (users/duration/ramp via CLI flags)
- Supervision tree for crash-resilient runs
- Metrics streaming (Telemetry events and Prometheus export) and thresholds
Each virtual user replays the whole scenario. How many times a user replays is controlled by two mutually exclusive config keys — set exactly one:
iterations— replay the scenario a fixed number of times, then stop (count based).test_duration— keep replaying the scenario until the duration elapses (time based). A replay already in progress always runs to completion, so a run may exceedtest_durationby at most one replay. Whentest_durationis set,iterationsis ignored.
For more than a single ramp up, set a profile instead. Every config key is
documented in the reference.
defmodule CountLoadTest do
use Typhoon
typhoon_config do
[
users: 100,
iterations: 50,
ramp_up_duration: Duration.new!(minute: 2),
ramp_down_duration: Duration.new!(minute: 2)
]
end
scenario "Protocol Handshake Test",
before_scenario: &setup/1,
after_scenario: &teardown/1 do
step("Establish Connection", &test_establish_connection/1, 1)
|> step("Login", &test_login/2, 1)
|> delay(Duration.new!(second: 15))
|> step("Send Packet", &test_send_packet/2, 1)
end
def setup(session), do: Map.put(session, :endpoint, "custom://localhost:8080/api")
def teardown(session), do: session
# 1-arity: ignores previous output, emits the connection as output.
def test_establish_connection(session) do
socket = connect(session.endpoint)
{session, socket}
end
def test_login(session, socket) do
token = login(socket)
{Map.put(session, :token, token), socket}
end
# 2-arity: uses the previous step's output (the token here is also in session).
def test_send_packet(session, socket) do
resp = send_packet(socket)
{Map.put(session, :resp, resp), socket}
end
typhoon_run do
["Protocol Handshake Test"]
end
endEvery step returns a {session, output} tuple: the session threads through the
whole scenario, and the output is handed to the next step. Runnable examples live
in test/load_test.
Run it with the typhoon.run Mix task:
mix typhoon.run path/to/load_test.exsThe scenario, hooks, steps and typhoon_run are the same as above; only the
config differs — swap iterations for test_duration:
typhoon_config do
[
users: 100,
test_duration: Duration.new!(minute: 30),
ramp_up_duration: Duration.new!(minute: 2),
ramp_down_duration: Duration.new!(minute: 2)
]
endmix test # the suite
mix compile --warnings-as-errors
mix formatCI runs all three on every push and pull request.
Enable the pre-commit hook once per clone — it runs mix format --check-formatted
on staged .ex/.exs files, so unformatted code never reaches CI:
git config core.hooksPath .githooksGit does not clone hook config, which is why this is a manual step. Skip a run
with git commit --no-verify.