Find the AWS EC2 instances that reach a crypto exchange with the lowest possible latency.
hft-hunter launches fleets of candidate EC2 instances across many instance types and racks (cluster placement groups), measures protocol-level latency to the exchange (TCP, TLS, HTTP, WebSocket, ICMP, UDP), ranks the candidates by their p99 latency, and automatically keeps the winners while terminating the losers. The result is a shortlist of the fastest instance types / placement groups on which you should run your production trading system.
There are three programs, and it matters which machine each one runs on:
| Program | Runs on | Who starts it | What it does |
|---|---|---|---|
hunter (CLI) |
Your operator machine — your laptop, or a small always-on control box | You, by hand | The control plane. Launches/terminates EC2 fleets, runs the collector, ranks results, keeps winners. |
hunter-agent |
Each AWS EC2 probe instance (the candidate servers you are evaluating) | Automatically, at instance boot | Repeatedly probes the exchange endpoints from that instance and uploads the measurements to the collector. You never install it by hand — the fleet launch does it for you. |
| collector | A host reachable from the EC2 probe instances, inside the same VPC | You, by hand (or embedded in hunter hunt) |
Receives probe measurements over HTTP and stores them as JSONL on disk. It is just hunter serve-collector. |
And two categories of servers:
- Target servers = the exchange endpoints, e.g.
api.binance.com,stream.binance.com,ws.okx.com. You deploy nothing here. These are external and are only measured. - Scanned / candidate servers = the AWS EC2 instances that hft-hunter launches for you.
The
hunter-agentruns on these, measuring how fast this specific instance in this specific rack can talk to the target servers.
Data flow:
your operator machine AWS VPC
┌───────────────────┐ ┌──────────────────────────────┐
│ hunter (CLI) │ EC2 │ probe instance ─┐ │ exchange
│ hunter fleet … │───────▶│ hunter-agent │ measures │──────▶ api.binance.com
│ hunter hunt … │ API │ probe instance │ latency │ ws.okx.com …
│ hunter report … │ │ hunter-agent ─┘ │ (target servers)
└───────────────────┘ │ │ uploads results │
▲ │ ▼ │
│ read JSONL │ collector (hunter │
└───────────────────┤ serve-collector) ◀───────────┘
└──────────────────────────────┘
- An AWS account with EC2 permissions, and credentials configured on your operator machine
(
aws configure, an SSO profile, or environment variables — anything the AWS SDK understands). - Terraform ≥ 1.5 (only to create the one-time network baseline).
- A Rust toolchain on your operator machine, only so you can build the two binaries once.
- Somewhere to publish the built
hunter-agentbinary so EC2 instances can download it at boot (an S3 bucket or any HTTPS URL works).
Building the code is a one-time setup step, not the daily workflow. If you only want to sanity check latency from your current machine, jump to Appendix A: local-only quick probe.
This is the main workflow. Follow it top to bottom the first time.
On your operator machine:
# The control-plane CLI you will run locally (native build is fine).
cargo build --release -p hunter
# The probe agent that will run ON the Linux EC2 instances.
# Build it for Linux so EC2 can execute it. If your operator machine is
# already Linux x86_64, a plain release build is enough:
cargo build --release -p hunter-agent
# On macOS / a different arch, cross-compile for the EC2 architecture, e.g.:
# rustup target add x86_64-unknown-linux-gnu
# cargo build --release -p hunter-agent --target x86_64-unknown-linux-gnuThe hunter binary lands in target/release/hunter; the agent in
target/release/hunter-agent (or under target/<triple>/release/).
Upload the hunter-agent binary to a URL the EC2 instances can reach at boot. For example:
aws s3 cp target/release/hunter-agent s3://your-bucket/hunter-agent
# then note its download URL, e.g. a presigned/HTTPS URL:
# https://your-bucket.s3.amazonaws.com/hunter-agentRemember this URL — it becomes agent_url in the config. At launch, each EC2 instance downloads
the agent from here and starts it as a systemd service.
The static VPC / subnet / security group / IAM profile is provisioned once with Terraform. The
per-instance lifecycle is handled later by the hunter CLI, not Terraform.
cd deploy/terraform
terraform init
terraform apply # review the plan, then confirm
# Copy these outputs — you paste them into the config in the next step:
terraform output subnet_id
terraform output security_group_idYou also need an AMI id (Amazon Linux 2023) for your region, and optionally an EC2 key pair name if you want SSH access to the instances.
The collector must be reachable from the probe instances, over the port in the security group (9900 by default, open inside the VPC). Practically that means running it on a host inside the VPC (a tiny always-on EC2 box, or your operator machine if it is reachable from the subnet).
On that collector host:
hunter serve-collector --listen 0.0.0.0:9900 --results-dir resultsNote the address as reachable from EC2 (a private VPC IP, e.g. 10.77.1.10:9900) — that becomes
collector_addr in the config.
Shortcut:
hunter hunt(Step 6) starts an embedded collector for you. If you usehunt, you can skip running a separate collector — just make surecollector_addrpoints at wherever thehunter huntprocess is reachable from EC2.
Edit the [fleet] section with the values gathered above:
[fleet]
region = "ap-northeast-1"
ami_id = "ami-xxxxxxxxxxxxxxxxx" # AL2023 AMI in your region
subnet_id = "subnet-xxxxxxxxxxxxxxxxx" # terraform output subnet_id
security_group_id = "sg-xxxxxxxxxxxxxxxxx" # terraform output security_group_id
key_name = "hft-hunter" # optional; your EC2 key pair
instance_types = ["c7i.xlarge", "c6in.xlarge", "c7gn.xlarge"]
count_per_type = 5 # instances launched per type
max_instances = 50 # hard cost guardrail
agent_url = "https://your-bucket.s3.amazonaws.com/hunter-agent" # Step 2
collector_addr = "10.77.1.10:9900" # collector as reachable from EC2
dry_run = false # set true to validate without launchingThe [[targets]] blocks define which exchange endpoints to measure, and the [hunt] block
controls the automated keep-the-winners loop (see Section 4).
The one-command, end-to-end path:
hunter hunt --config config/hunter.tomlThis will:
- Start an embedded collector.
- Launch the first wave of EC2 instances (one cluster placement group per instance type).
Each instance boots, tunes its OS/NIC, downloads and runs
hunter-agent. - Wait
wait_secswhile agents probe the exchange and upload results. - Rank the instances by p99 latency and print a Markdown table.
- Keep the top
keep_topwinners, terminate the rest, then (for furtherrounds) add more instances into the winning placement groups and re-test — hunting for the fastest rack.
At the end you have the winning instance ids / placement groups: that is where you should run your production trading system.
# Re-render a report from collected results at any time:
hunter report --input results --format md # Markdown table
hunter report --input results --format csv --top 3 # top 3 as CSV
hunter report --input results --format json # JSON
# If you ran waves manually (see below) and want to stop paying for them:
hunter fleet terminate --config config/hunter.toml --wave w1The behaviour is driven by the [hunt] block in config/hunter.toml:
[hunt]
wave = "w1" # label used for tags and placement group names
wait_secs = 300 # how long to wait for agents to report each round
keep_top = 3 # how many winners to keep each round
rounds = 2 # 1 = single round; >1 refines the winning racks
expand_per_round = 4 # instances added to each winning placement group per roundEach round: wait → rank by p99 → keep the top keep_top, terminate the losers → expand the
winning placement groups with expand_per_round fresh instances → repeat. This progressively
concentrates your candidates in the fastest racks.
If you prefer to drive each stage yourself:
# 1. Start the collector on a VPC-reachable host (see Step 4).
hunter serve-collector --listen 0.0.0.0:9900 --results-dir results
# 2. Launch a wave (optionally override instance types / count from the CLI).
hunter fleet launch --config config/hunter.toml --wave w1
hunter fleet launch --config config/hunter.toml --wave w1 --types c7i.xlarge,c6in.xlarge --count 4
# 3. Watch the instances come up.
hunter fleet status --config config/hunter.toml --wave w1
# 4. Once enough results have arrived, rank them.
hunter report --input results --format md --top 5
# 5. Terminate the wave when you are done.
hunter fleet terminate --config config/hunter.toml --wave w1Each target lists the protocols to measure in its protocols = [...] field:
| Kind | Meaning |
|---|---|
tcp_connect |
Time to complete the TCP three-way handshake |
tls_handshake |
Time to complete the TLS handshake |
http |
HTTP time-to-first-byte (uses the target's http_path) |
ws_ping |
WebSocket ping/pong round trip (uses ws_path) |
ws_order_create |
WebSocket CREATE_ORDER -> BOOKED round trip (needs a mock/order endpoint) |
ws_order_cancel |
WebSocket CANCEL_ORDER -> DONE round trip (needs a mock/order endpoint) |
icmp |
ICMP echo (ping) round trip |
udp_rtt |
UDP echo round trip |
Ready-made target sets live in config/targets/binance.toml and config/targets/okx.toml.
You do not need AWS to try hft-hunter. To measure latency straight from the machine you are on right now:
# Discover the IPs behind an exchange hostname and their TCP connect times.
hunter discover --host api.binance.com
# Run every probe in the config from this machine and print a ranking.
hunter probe --config config/hunter.tomlTo exercise the WebSocket order round-trip probe locally, start the bundled mock exchange first:
mock-exchange --listen 127.0.0.1:9001 --udp-listen 127.0.0.1:9002
# then point a target at 127.0.0.1:9001 with
# protocols = ["ws_ping", "ws_order_create", "ws_order_cancel"]mock-exchange is only a local testing/BDD helper — it plays no role in a real AWS hunt.
| File | Purpose |
|---|---|
config/hunter.toml |
Main config: probe targets, [fleet] AWS settings, [collector], [hunt] workflow |
config/targets/binance.toml |
Standalone Binance target set (REST + WS) |
config/targets/okx.toml |
Standalone OKX target set (REST + WS public/private) |
deploy/terraform/ |
One-time AWS baseline (VPC, subnet, security group, IAM) |
For development work on hft-hunter itself:
just setup # install dev tools (cargo-shear, cargo-sort, typos-cli)
just build # cargo build --workspace
just lint # typos + markdown + sort check + fmt check + clippy + shear
just test # unit tests + proptest
just bdd # cucumber-rs BDD scenarios (stats, probe, report)
just test-all # test + bdd
just docs # cargo doc --no-deps --openbin/
hunter Control-plane CLI (discover, probe, serve-collector, fleet, hunt, report)
hunter-agent Probe daemon that runs on EC2 instances
mock-exchange Local mock WS + UDP exchange for testing/BDD
crates/
common Shared domain types (ProbeKind, Target, ProbeRecord, ...)
probe TCP/TLS/HTTP/WS/ICMP/UDP probing engine
stats HDR-histogram nanosecond latency statistics
collect Axum collector service + JSONL storage + EC2 IMDS metadata
report p99 ranking and Markdown/CSV/JSON rendering
cloud AWS EC2 fleet orchestration (launch, keep/kill, placement groups)
mockex Mock exchange server library
Apache-2.0