How-to guide

Queuey Edge — durable publishing from unreliable networks

For producers where the network is a fact of life, not a guarantee — factories, kiosks, vehicles, ships, on-prem services. One PublishAsync call durably accepts the event on the local machine; Queuey owns the delivery mechanics from there.

The contract

When PublishAsync returns, the event is committed to a local durable store (SQLite, fsync'd) under a permanent transfer identity. Queuey then handles transfer to Queuey Cloud, retries with backoff and jitter, reconnect, lost-acknowledgement resolution, idempotent resend, recovery across process and machine restarts, and backlog draining — in order. Kill the network for two days: events accumulate durably and drain when it returns. A retry, however late, can never become a second logical event.

Before Queuey.Edge
// ❌ What Queuey.Edge exists to delete from your codebase
if (!await queuey.IsOnline())
{
    await localDb.Save(evt);
    ScheduleRetry(...);
}

Set it up once

Install the Queuey.Edge NuGet package, register it, and mint a publish-only, tenant-scoped API key in the console (Developer → API keys) — a key on an edge machine should never be able to do anything but publish to its own workspace.

Program.cs
// Program.cs — any .NET host (worker service, ASP.NET, console)
builder.Services.AddQueueyEdge(o =>
{
    o.ApiKey = "qak_…";           // a PUBLISH-ONLY, tenant-scoped Edge key
    o.TenantPublicId = "ten_…";
    // Optional: o.Storage.Path = "/var/lib/myapp/queuey/spool.db";
});

Then publish — that's the whole app

SensorService.cs
// The whole delivery story in application code:
public sealed class SensorService(IQueueyPublisher queuey)
{
    public async Task OnReading(Reading reading) =>
        await queuey.PublishAsync("sensor-readings", reading, new PublishOptions
        {
            EventType = "temperature.updated",
            GroupKey = reading.DeviceId,        // ordering lane
            OccurredAtUtc = reading.ReadAtUtc,  // honest history when a backlog drains
        });
}
Honest history
Pass OccurredAtUtc and a backlog drained on Wednesday still reads as Monday in the console — Queuey stores the occurrence time separately from the receive time (X-Queuey-Occurred-At).

What can actually go wrong at the call

PublishAsync throws only for conditions that exist before Queuey takes responsibility: missing configuration, a payload that cannot be serialized or exceeds your local cap, a full local store (QueueySpoolFullException — the honest backpressure of lossless retention), or a corrupt store awaiting explicit operator recovery. Network state, Queuey Cloud availability and HTTP errors never surface at the call site.

Storage rules
Local durable disk only (never a network share; in containers, a persistent volume), and exclude spool.db* from antivirus and file-copy backups. Default 512 MB spool ≈ a year of offline autonomy at 1 event/minute × 1 KB.

Queuey exposes health — you monitor it

Edge publishes an OpenTelemetry meter and an in-process snapshot for whatever monitoring you already run (Azure Monitor, Datadog, Prometheus, SCADA). If you wire exactly one alert, use oldest_age_seconds: it grows when anything has been stopping transfer, and Edge resumes by itself when the cause clears.

Health signals
Meter: Queuey.Edge (OpenTelemetry)

  queuey.edge.spool.pending               events accepted locally, not yet transferred
  queuey.edge.spool.oldest_age_seconds    ← THE signal worth alerting on
  queuey.edge.spool.quarantined           permanently rejected, awaiting operator retry/discard
  queuey.edge.cloud.last_contact_seconds  seconds since last successful transfer
  queuey.edge.state                       0 Healthy · 1 Backlogged · 2 RequiresAction
                                          3 StorageFull · 4 StorageFaulted
  queuey.edge.transfer.accepted{replayed} / transfer.failed{class,reason}

Operator verbs

The queuey CLI operates a spool file directly, alongside a running host. Discard reaches only quarantined events, and data loss is always an explicit, acknowledged decision — never Queuey's default.

CLI
queuey edge status  --spool <path> [--json]
queuey edge retry   --spool <path> (--id N | --all)    # after fixing a quarantine cause
queuey edge discard --spool <path> --id N              # explicit, logged operator decision
queuey edge recover --spool <path>                     # salvage a faulted spool
queuey edge reset   --spool <path> --accept-data-loss  # start clean; old file preserved
Next steps