Tyto.Transports.Postgres 0.0.1-alpha.97

This is a prerelease version of Tyto.Transports.Postgres.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Tyto.Transports.Postgres --version 0.0.1-alpha.97
                    
NuGet\Install-Package Tyto.Transports.Postgres -Version 0.0.1-alpha.97
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tyto.Transports.Postgres" Version="0.0.1-alpha.97" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tyto.Transports.Postgres" Version="0.0.1-alpha.97" />
                    
Directory.Packages.props
<PackageReference Include="Tyto.Transports.Postgres" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Tyto.Transports.Postgres --version 0.0.1-alpha.97
                    
#r "nuget: Tyto.Transports.Postgres, 0.0.1-alpha.97"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Tyto.Transports.Postgres@0.0.1-alpha.97
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Tyto.Transports.Postgres&version=0.0.1-alpha.97&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Tyto.Transports.Postgres&version=0.0.1-alpha.97&prerelease
                    
Install as a Cake Tool

Tyto.Transports.Postgres

PostgreSQL-backed transport for Tyto. Designed for teams that want a real queue (ack/nack, retry, dead-letter, competing consumers) without operating Kafka or RabbitMQ — when you already run Postgres, this reuses it as the broker.

This is not a Kafka replacement. It targets thousands-of-messages/second workloads where operational simplicity and transactional integrity matter more than raw broker throughput.

How it works

A single table acts as the queue:

column purpose
id identity PK
queue endpoint address (logical queue)
payload / headers / message_* the MessageEnvelope
state 0=Ready, 1=InFlight, 2=DeadLetter
attempt delivery count, drives retry/dead-letter
visible_at delayed-visibility gate
locked_until crash-recovery lease guard
  • SendINSERT a Ready row, then pg_notify to wake consumers.
  • ReceiveSELECT ... FOR UPDATE SKIP LOCKED leases a batch and marks it InFlight with a locked_until lease. Consumers also LISTEN for instant wake-ups and fall back to polling (PollingInterval).
  • SettleComplete deletes the row; Abandon/retry bumps attempt and delays visible_at; exceeding MaxRetryCount sets state = DeadLetter.
  • Crash safety → an InFlight row whose locked_until has passed is re-leased automatically (at-least-once delivery).

The table, schema and index are created idempotently at startup by PostgresTopologyManager (set AutoCreateSchema = false to run DDL-free under least-privilege credentials).

Priority queues

Each message carries a priority (default PostgresOptions.DefaultPriority, or a per-message Tyto-Priority header). Higher values are leased first — the lease order is priority DESC, visible_at, id, backed by a matching index.

// per-message
envelope.Headers["Tyto-Priority"] = "10";
// or a transport-wide default
pg.Configure(o => o.DefaultPriority = 0);

Retries, backoff & poison messages

A failed message is settled by policy, keyed off the DB attempt column (crash-safe — no reliance on headers):

  • Backoff: RetryStrategy is Exponential by default — the delay is RetryDelayMilliseconds * 2^(attempt-1), capped at MaxRetryDelayMilliseconds, with ±RetryJitterFactor random jitter to avoid retry stampedes. Set RetryStrategy = Fixed for a constant delay.
  • Give up: once attempt >= MaxRetryCount the message is dead-lettered.
  • Poison messages: exceptions that can never succeed on retry (validation, deserialization) can be dead-lettered immediately via NonRetryableExceptions or a custom ShouldRetry predicate — no wasted retries, no queue head-of-line blocking. The dead-letter reason records whether it was exhaustion or a non-retryable exception.
pg.Consumers(c => {
    c.MaxRetryCount = 5;
    c.RetryDelayMilliseconds = 1000;      // base delay
    c.RetryStrategy = PostgresRetryStrategy.Exponential;
    c.MaxRetryDelayMilliseconds = 30_000; // cap
    c.RetryJitterFactor = 0.2;            // ±20%

    // Poison-message protection: never retry these.
    c.NonRetryableExceptions.Add(typeof(ValidationException));
    c.NonRetryableExceptions.Add(typeof(JsonException));
    // or a predicate:
    c.ShouldRetry = ex => ex is not ArgumentException;
});

With the defaults above and base 1s: retries land at ~1s, ~2s, ~4s, ~8s … (each ±20%), until MaxRetryCount, then dead-letter.

TTL / expiry & retention

  • Messages with ExpiresAt in the past are never leased and are reclaimed by the maintenance worker.
  • Completed messages are deleted on Complete, so they never accumulate.
  • Dead-lettered rows are purged after Maintenance.DeadLetterRetention (default 7 days; TimeSpan.Zero keeps them forever for inspection).

PostgresMaintenanceWorker runs these sweeps on Maintenance.Interval (default 5 min), keeping table bloat — and the resulting autovacuum pressure — in check.

pg.Configure(o => {
    o.Maintenance.Interval = TimeSpan.FromMinutes(2);
    o.Maintenance.DeadLetterRetention = TimeSpan.FromDays(3);
    o.Maintenance.DeleteExpiredMessages = true;
});

Observability (OpenTelemetry)

Meter and ActivitySource are both named Tyto.Transports.Postgres.

  • Traces: postgres.send (producer), postgres.receive (consumer).
  • Metrics: enqueued, leased, completed, abandoned, deadlettered, expired, retention.deleted counters and a processing.duration histogram (all under tyto.transports.postgres.*).
tracing.AddSource("Tyto.Transports.Postgres");
metrics.AddMeter("Tyto.Transports.Postgres");

Usage

services.AddTyto(tyto => {
    tyto.Transports(t => {
        t.AddPostgres("Postgres_Main", pg => {
            pg.Connection("Host=localhost;Database=app;Username=app;Password=secret");
            pg.Schema("public");        // optional, default "public"
            pg.Table("tyto_messages");  // optional
            pg.Consumers(c => {
                c.DefaultConcurrencyLimit = 4;
                c.MaxRetryCount = 5;
                c.RetryDelayMilliseconds = 2000;
                c.PerQueueSettings["orders"] = new() { ConcurrencyLimit = 8 };
            });
        });
    });
    // ... endpoints route to "Postgres_Main" like any other transport
});

Transactional outbox seam

IPostgresMessageStore.EnqueueAsync accepts an optional caller-owned NpgsqlConnection / NpgsqlTransaction. When supplied, the message is inserted in the same transaction as the caller's business data — the foundation for a true transactional outbox (message and state commit atomically, or not at all).

The current "publish now" path (PostgresSendingTransport) passes null, opening its own connection. When the outbox module is wired into Tyto's send pipeline, it can call the store directly with its transaction — no transport changes required.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0-alpha.5 0 9/21/2026
0.1.0-alpha.4 33 9/20/2026
0.1.0-alpha.3 33 9/20/2026
0.1.0-alpha.2 41 9/20/2026
0.1.0-alpha.1 36 9/20/2026
0.0.1-alpha.106 39 9/15/2026
0.0.1-alpha.105 69 9/14/2026
0.0.1-alpha.104 55 9/10/2026
0.0.1-alpha.103 57 9/4/2026
0.0.1-alpha.102 54 9/1/2026
0.0.1-alpha.101 51 9/1/2026
0.0.1-alpha.100 62 8/24/2026
0.0.1-alpha.99 62 8/20/2026
0.0.1-alpha.98 62 8/18/2026
0.0.1-alpha.97 60 8/18/2026
0.0.1-alpha.96 96 8/18/2026
0.0.1-alpha.95 68 8/17/2026
0.0.1-alpha.94 66 7/21/2026
0.0.1-alpha.93 58 7/20/2026
0.0.1-alpha.92 59 7/20/2026