APM v2.12.0

Linux static binaries compiled 2026-10-10. Copy, chmod +x, run. Zero dependencies.

← All releases

Upgrading an existing install? — Since v2.8 a worker with no user line in a root-owned path refuses to start instead of silently running as root, and the CLI needs root or membership of group apm. Run sudo apm healconf after installing: it checks your config and every include, explains each setting this version reads differently, and fixes it with a backup beside the file (--check only reports; --yes applies every safe fix). apm upgrade runs the check for you.
Windows Defender false positive — Microsoft Defender may flag APM binaries as Wacatac.B!ml. This is a known false positive: its ML heuristic misfires on statically compiled Go binaries and UPX-compressed executables. It has been reported to Microsoft for review. All other major antivirus engines report these files as clean — see the VirusTotal links below for full scan results.
Linux
x86_64 / amd64
apm-v2.12.0-x64 standard 9.1 MB
SHA-256: e3fd4a464c71b020a8e913d469dc3f620c2c855b77b670173ab30bfab95cc549 VirusTotal (pending)
apm-v2.12.0-x64-upx UPX 2.8 MB
SHA-256: 14a87b79d2f146a2cbbaa7ea0e402940d04a5d8d83721883a6a024f731d0abdd VirusTotal (pending)
Linux
ARM64 / aarch64
apm-v2.12.0-arm64 standard 8.4 MB
SHA-256: 24ed1bf2f679e39e712eb0b254b66f3eead71eeb7469a63abfe81e0a7ae0d68a VirusTotal (pending)
apm-v2.12.0-arm64-upx UPX 2.4 MB
SHA-256: edaa4ca51e5a6b2231c5de1ed36bbf723881095e3eae9343412eea563ddc028f VirusTotal (pending)
Linux
ARMv7 / armhf
apm-v2.12.0-armhf standard 8.8 MB
SHA-256: ae55d78bf5498087f07829396438dc7f84f070e9f2ddb74b22bb7c3d912971f8 VirusTotal (pending)
apm-v2.12.0-armhf-upx UPX 2.4 MB
SHA-256: 810a13be8d58832612eb41f6ae584c4e9907956cbc7effa62715505d40f11f01 VirusTotal (pending)
This release is signed. metadata.json (the SHA-256 of every file above), install.sh and each connector carry an ed25519 signature beside them as <file>.sig. The public key is compiled into every APM binary since v2.10: apm upgrade refuses anything that does not verify, and apm verify <file> <file>.sig checks a download by hand. Key fingerprint (SHA-256 of the public key): f6af819d1b56bcc075a009f5051bb3a4bcf5d0cd962b513d763898a8ca09527f
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAJaQi2HxEjH2ncrimkjGwyyj1XYYwfqTtSPuNT2VIiy8=
-----END PUBLIC KEY-----
To check with OpenSSL: openssl pkeyutl -verify -pubin -inkey apm.pub -rawin -in metadata.json -sigfile <(base64 -d metadata.json.sig)

v2.12.0 — a vanished device is reported in seconds, through any proxy, per device

A WebSocket client that vanishes — power cut, cable pulled, firmware wedged — sends nothing. dead_timeout (v2.6) catches that with kernel probes, but only on a listener APM terminates itself; behind nginx, a unix socket or Cloudflare the TCP peer is the proxy, which is alive, so the worker heard nothing until the keep_alive deadline, 120 s, or until the application's own heartbeat gave up. That chain is exactly what a device fleet behind a CDN runs.

ping_interval / ping_timeout / ping_miss — the fleet's dead-device detector

worker {
    server         ws://unix:./fleet.sock;
    ping_interval  10000;  # ping after 10 s of silence …
    ping_timeout   2000;   # … wait 2 s for the pong (and between retries) …
    ping_miss      2;      # … two unanswered pings and the client is gone: 14 s
}

After ping_interval of silence APM sends a ping and waits ping_timeout; ping_miss unanswered pings in a row and the client is closed with code 1001 (ping timeout) and the worker gets connectionClosed, exactly as for any dirty disconnect — no heartbeat code in the application. Any frame from the client is proof of life, so a talking connection is never pinged, and the per-connection ping goroutine is gone: the read deadline is the timer. Control frames are forwarded end to end by nginx and by Cloudflare, so the only requirement is a client that answers pings, which every browser and standard WebSocket library does. Unset, the timing is what every earlier version had (ping after keep_alive/2, closed at keep_alive), minus the pings a busy connection used to receive every 60 s regardless.

Measured on a real link cut: 7–9 s through Cloudflare → nginx → unix socket at ping_interval 5000, 3 s direct at 3000, 2 s with dead_timeout on top; on defaults still 103–119 s, and Cloudflare itself never reported a dead client. Also learned: Cloudflare closes a WebSocket with no frames at all after ~125 s and counts ping/pong as traffic.

Per connection, and latency for free — connectors v3.4.0

session.setPing({ interval, timeout, miss }) (set_ping in Python, Perl and Lua) replaces the policy for one session, so a fleet can give some devices faster detection than others. Every ping now carries its send time and the pong echoes it, so each cycle measures the client's round trip; session.pingStats() returns the last sample and avg/min/max/count over the last 5 and 60 minutes, kept per connection in the daemon, and a built-in ws_rtt timing metric per worker shows the fleet in the GUI and on StatsD. All six connectors move to v3.4.0; the conformance harness covers both calls.

Upgrade notes: nothing changes unless you set ping_interval. Device fleets behind a proxy or CDN: set ping_interval 10000; ping_timeout 2000; ping_miss 2; on the worker and drop the application-level heartbeat if it was only there for liveness. Details in the timestamped notes beside this file.