Delivery you can reason about.
The signed push is ordered and at-least-once, with a dead-letter table for what a receiver will never accept. Here is exactly what happens to a record, including where the guarantees stop.
- Persist firstWith the durable queue enabled, each record is written to push_queue.db (SQLite, WAL) before any HTTP attempt. A crash between "produced" and "sent" loses nothing.
- Send in orderThe drain task sends rows strictly in id order. A failed row sets a retry deadline and later rows wait behind it; nothing is sent ahead of an earlier failure.
- Settle by responseThe row leaves the queue only when the receiver answers.
- 2xx, 409Acknowledged. Row deleted.
- 408, 429, 5xx, timeoutExponential backoff, honoring Retry-After, capped at 300 s. The row stays.
- 401, 403Credential problem. Backs off and keeps the row.
- other 4xxPermanent. The row moves to the dead-letter table and delivery continues.
What you can rely on
- Dead letters are inspectable and replayable: GET /api/push/dead, POST /api/push/dead/replay, or DELETE to purge.
- Batched pushes carry a persisted, monotonic sequence and an X-Batch-Id. Retries are byte-identical.
- On SIGTERM it stops adapters, drains the executor, flushes sinks and the push queue, all inside a 30 s budget. The shipped systemd unit allows 45 s.
- NATS JetStream input is acked after the record is durably handled, nak-ed with backoff when a sink fails, and termed when a retry cannot help.
- If the data directory is not writable and persistence was requested, startup fails instead of running volatile.
Where it stops
- Delivery is at-least-once. After a timeout or a crash the receiver can see a record twice. Dedupe on (machine.id, timestamp, source), or on X-Batch-Id for batches.
- The durable queue is opt-in (push_queue.enabled). Without it the retry buffer is in memory, holds 1000 entries and is lost on restart.
- The queue is bounded by a disk quota (default 500 MB). At the quota the oldest rows are evicted, and the gap is logged and counted.
- MQTT input is acknowledged on receipt. Route devices through NATS JetStream when ingest must survive a restart.
Questions about delivery.
Something missing? Write to marketing@opexcg.com.
What happens when the receiving endpoint is down?
With the durable queue enabled, records are persisted to SQLite before they are sent, then retried in order with backoff until the receiver answers. Permanently rejected rows go to the dead-letter table, where you can inspect and replay them.
The queue is bounded by a disk quota (default 500 MB). At the quota the oldest rows are evicted and the gap is counted. Without the durable queue the retry buffer is in memory and is lost on restart.
Will my receiver see duplicates?
It can. Delivery is at-least-once: if a request times out, the retry may duplicate a record that was in fact processed. Dedupe on (machine.id, timestamp, source), or on the X-Batch-Id header when you use batching.
Does it feed OpexMX?
Yes. The signed webhook sink posts to the OpexMX inbound webhook. The same records can fan out to NATS, MQTT, ClickHouse, Postgres or any HTTP receiver that verifies the signature.
From download to first record in minutes.
Run one binary, connect a PLC, a CNC or a webhook, and watch the records arrive in the editor.
- One binary
- Raspberry Pi to rack server
- Signed delivery with retries
Sizing questions or an air-gapped install? Email marketing@opexcg.com or read the quick start.