/api/qmail/local/beacon/control
GET POSTRuntime control for the background beacon polling thread. Lets a tester or the GUI pause, resume, or query the thread without restarting the daemon. Useful for deterministic /api/qmail/net/beacon/ping testing (the manual long-poll won't race the background thread) and for in-prod troubleshooting.
Description
The /api/qmail/local/beacon/control endpoint is a thin wrapper over the singleton g_qmail_beacon_state background thread. The same JSON response is returned for all three actions, so a caller can confirm the state transition with a single call.
- Deterministic /ping testing: the background thread polls the beacon RAIDA on its own cadence. If the thread fires while a test is awaiting
/ping, the manual call returns immediately with whatever the thread just consumed. Stop the thread first to pin down behavior. - Triage: stop the beacon for a minute, manually drive PEEK/PING/DOWNLOAD, then resume to confirm the background loop recovers cleanly.
- Quiet logs: the background thread logs every poll cycle. Stopping it temporarily makes
main.logeasier to read while reproducing an unrelated issue.
This endpoint changes runtime state only — it does not update any config file. After a daemon restart, the beacon thread is started or skipped according to the boot-time beacon_auto_start config option (Section E1 of the QMail v3 plan).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action |
enum | No | One of start, stop, or status. Defaults to status if omitted or empty. Any other value returns HTTP 400. |
start on an already-running thread is a no-op. stop on an already-stopped thread is a no-op. Both still return the current status, so you can poll ?action=status to confirm before issuing a transition.
Response
All three actions return the same JSON shape — the current beacon state. The running field reflects state after the requested action.
Success Response Properties
true on a successful transition or query.true if the background beacon polling thread is currently running.0 if none has arrived since startup.0 when the beacon is healthy.Success Response Example (status — running)
{
"success": true,
"running": true,
"beacon_raida": 11,
"serial_number": 55077,
"denomination": 1,
"last_tell_timestamp": 1735689812,
"backoff_seconds": 1,
"consecutive_errors": 0
}
Success Response Example (after stop)
{
"success": true,
"running": false,
"beacon_raida": 11,
"serial_number": 55077,
"denomination": 1,
"last_tell_timestamp": 1735689812,
"backoff_seconds": 1,
"consecutive_errors": 0
}
Error Response — Invalid Action (HTTP 400)
{
"error": true,
"message": "Invalid action: use start, stop, or status",
"code": 400
}
Error Response — Restart Failed (HTTP 500)
{
"error": true,
"message": "Failed to start beacon thread",
"code": 500
}
Interactive API Tester
Test this Endpoint
Examples
cURL — All Three Actions
# Query without changing state
curl "http://localhost:8082/api/qmail/local/beacon/control?action=status"
# Pause the background polling thread
curl "http://localhost:8082/api/qmail/local/beacon/control?action=stop"
# Resume it
curl "http://localhost:8082/api/qmail/local/beacon/control?action=start"
JavaScript (fetch)
// Pause the beacon, run a deterministic /ping test, then resume.
async function withBeaconStopped(testFn) {
await fetch('http://localhost:8082/api/qmail/local/beacon/control?action=stop').then(r => r.json());
try {
return await testFn();
} finally {
await fetch('http://localhost:8082/api/qmail/local/beacon/control?action=start').then(r => r.json());
}
}
const result = await withBeaconStopped(async () => {
return fetch('/api/qmail/net/beacon/ping?timeout=10').then(r => r.json());
});
console.log('Ping result:', result);
Python
import contextlib
import requests
BASE = 'http://localhost:8082/api'
@contextlib.contextmanager
def beacon_stopped():
requests.get(f'{BASE}/qmail/local/beacon/control', params={'action': 'stop'})
try:
yield
finally:
requests.get(f'{BASE}/qmail/local/beacon/control', params={'action': 'start'})
with beacon_stopped():
# The background thread is paused — /ping won't race it.
r = requests.get(f'{BASE}/qmail/net/beacon/ping', params={'timeout': 10})
print('Ping result:', r.json())
Important Notes
statusIf action is missing or empty, the handler treats it as status — a side-effect-free read. Hit the URL without any params for a quick health check.
The background beacon thread polls on its own cadence and consumes pending Tells from the beacon RAIDA. If a test is set up to fire a /ping and assert what it sees, an unrelated background poll between fixture setup and the assertion can race the test. Stopping the thread for the duration of the test fixes that — see the example above.
While the beacon is stopped, the daemon does not consume new Tell notifications. If you stop the thread and forget to resume it, downstream features like inbox auto-population stop working until the next daemon restart. Use the context-manager pattern shown in the JavaScript/Python examples to ensure resume always happens.