Task: run the TMS heartbeat load simulator from this machine, pointed at a broker on another host

Context. A Phoenix/Elixir app (da_product_app), its MySQL DB, and an MQTT broker are all running together on the origin host. We're testing how many simulated terminals it can handle before that shared host becomes the bottleneck (it capped out around 15k devices when the load generator ran on the same box as the broker/app/DB). The next step is running the load generator here, on a separate machine, so it no longer competes with the app for CPU. You are running only the simulator — do not touch the app, DB, or broker; another session owns those and is watching the app-side logs and DB during the run.

Target broker: 78.47.213.212, port 1883, plain MQTT (not TLS) — confirmed by a raw MQTT CONNECT/CONNACK probe, not just the port number.

Step 0 — verify reachability before anything else:


import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(("78.47.213.212", 1883))
s.send(bytes([0x10,0x10,0x00,0x04])+b"MQTT"+bytes([0x04,0x02,0x00,0x3C,0x00,0x04])+b"test")
print(s.recv(64))  # expect b' \x02\x00\x00' = CONNACK, accepted
If this times out or refuses, stop and report that back — it means a firewall/security group between this host and the origin is blocking port 1883, and that needs fixing before any load test is meaningful.

Setup:


git clone https://github.com/momentpay/tmsuat_apps.git
cd tmsuat_apps
git checkout feat/structured-logging
cd tools/terminal-heartbeat
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
You do not need LT_DB_PASSWORD, and do not run seed.py or cleanup.py — those touch the shared DB and the origin session owns that.

Smoke test first, always:


LT_MQTT_HOST=78.47.213.212 LT_MQTT_PORT=1883 LT_MQTT_CLIENT_PREFIX=loadtest-remote-<yourhostname> \
  ./.venv/bin/python simulator.py --smoke
Confirm zero connect_failures/publish_errors before scaling up.

Real runs, same pattern, one device count at a time (5k, 10k, 15k, 20k, 25k — whatever we ask for next):


LT_MQTT_HOST=78.47.213.212 LT_MQTT_PORT=1883 LT_MQTT_CLIENT_PREFIX=loadtest-remote-<yourhostname> \
  ./.venv/bin/python run_guarded.py <device_count> --duration 300
LT_MQTT_CLIENT_PREFIX must be unique to your run (hostname + timestamp is fine) — this broker may see other test traffic, and reusing client ids causes connection churn.

Terminal serials: the simulator generates its own LOADTEST-XXXXXX serials and vendor LoadTest/model mf919 — you don't need to know or coordinate specific terminal IDs with the app side; the app creates those terminal rows on first heartbeat automatically. Just don't change LT_DEVICE_COUNT naming/prefix logic in simulator.py.

What to report back, per run: the full JSON block run_guarded.py prints at the end (heartbeats_sent, connect_failures, publish_errors, pushes_received, duplicate_pushes, gap_devices_never_pushed, latency_seconds_by_type), plus how long the initial connection ramp took (the "Connected N/N workers in Xs" line) — that ramp time is itself a signal of network latency/quality to the origin host.

Known caveat to mention if numbers look off: if a prior run's push logs are still within their 30-minute cooldown or not yet timed out, a :outdated gap may briefly get skipped rather than repushed — this self-clears within about 5–6 minutes and isn't something to fix on your end.