A Big Week for the Critter Stack โ
The post-GA cadence has not let up. Between June 22 and June 29, we shipped three Wolverine releases, three Marten releases, and three Polecat releases โ a week heavy on database-backed messaging, brand-new interoperability with the rest of the .NET messaging ecosystem, and a steady drumbeat of work to make every part of the stack more observable and more manageable from CritterWatch.
Here's a tour of what landed.
Release Timeline โ
| Day | Wolverine | Marten | Polecat |
|---|---|---|---|
| Jun 22 | โ | 9.10.0 | โ |
| Jun 23 | 6.14.0 | โ | 4.5.2 |
| Jun 25 | โ | โ | 4.6.0 |
| Jun 26 | 6.15.0 | 9.11.0 | โ |
| Jun 29 | 6.16.0 | 9.12.0 | 4.7.0 |
CritterWatch โ
(Coming โ written separately.)
Wolverine โ
Three releases this week, but the headline is clear: database-backed messaging got faster, and Wolverine now speaks the queueing protocols of the two biggest .NET messaging frameworks.
๐ Database queue performance โ
Both the PostgreSQL and SQL Server transports got a focused performance pass that targets the hottest part of any database-backed queue: the dequeue path.
- PostgreSQL transport โ indexed dequeue path plus more robust idempotency handling (#3278).
- SQL Server transport โ indexed dequeue path plus an opt-in clustered queue layout so the physical table organization matches how the queue is actually read (#3277).
On SQL Server the new layout is one fluent call. Clustering the queue and scheduled tables on a monotonic seq identity (instead of the previous random-Guid clustered key) turns FIFO dequeue into a clustered seek with physically contiguous deletes:
opts.UseSqlServerPersistenceAndTransport(connectionString)
.OptimizeQueueThroughput();The raw-DDL benchmark behind the PR tells the story โ same hardware, same workload:
| Layout | Throughput | p50 latency | p99 latency |
|---|---|---|---|
baseline (clustered Guid, no index) | 98/s | 845 ms | 1,860 ms |
OptimizeQueueThroughput() (clustered seq) | 34,612/s | 2.4 ms | 3.7 ms |
If you lean on Wolverine's database queues โ whether as a no-broker option or to keep messaging transactionally consistent with your business data โ the indexed dequeue path is a free win on upgrade. OptimizeQueueThroughput() is opt-in specifically because enabling it on an existing database triggers a one-time queue-table rebuild, so it's a maintenance-window change for existing systems and a no-brainer for new apps.
๐ SQL Server transport docs ยท ๐ PostgreSQL transport docs
๐ Interop with MassTransit and NServiceBus over SQL Server and PostgreSQL โ
This is the big one. Wolverine can now send to and receive from MassTransit and NServiceBus applications using each framework's own SQL Server or PostgreSQL queueing โ reading and writing their native tables directly, no shared broker required.
Landed across 6.14.0 and 6.16.0:
- NServiceBus over SQL Server (#3198)
- NServiceBus over PostgreSQL (#3201)
- MassTransit over PostgreSQL (#3203)
- Each interop transport is pinned to a dedicated database under multi-tenanted storage (#3271),
Seqis indexed on the NServiceBus PostgreSQL queue table (#3205), and a sharedDatabaseListenerbase now backs the polling loop across all of these (#3206).
For NServiceBus, Wolverine reads and writes the NServiceBus queue tables directly โ one table per queue with a JSON Headers column and a raw Body column:
using Wolverine.SqlServer.Transport.NServiceBus;
builder.UseWolverine(opts =>
{
// Wolverine's own durable inbox/outbox still lives in SQL Server
opts.PersistMessagesWithSqlServer(connectionString, "wolverine");
opts.UseNServiceBusSqlServerInterop();
// Publish to an NServiceBus endpoint's queue table
opts.PublishMessage<OrderPlaced>().ToNServiceBusSqlServerQueue("nsb");
// Listen to Wolverine's own queue table and use it for replies
opts.ListenToNServiceBusSqlServerQueue("wolverine").UseForReplies();
// Bind NServiceBus interface-typed messages to Wolverine's concrete types
opts.Policies.RegisterInteropMessageAssembly(typeof(IOrderContract).Assembly);
});PostgreSQL is identical with the UseNServiceBusPostgresqlInterop() / ListenToNServiceBusPostgresqlQueue() / ToNServiceBusPostgresqlQueue() trio. MassTransit is a different shape โ its SQL transport is a function-driven, two-table model (transport.message + transport.message_delivery) that MassTransit owns and migrates, so Wolverine calls its stored functions rather than touching a table:
using Wolverine.Postgresql.Transport.MassTransit;
builder.UseWolverine(opts =>
{
opts.PersistMessagesWithPostgresql(connectionString, "wolverine");
opts.UseMassTransitPostgresqlInterop(autoProvision: true);
opts.PublishMessage<OrderPlaced>().ToMassTransitPostgresqlQueue("masstransit");
opts.ListenToMassTransitPostgresqlQueue("wolverine").UseForReplies();
opts.Policies.RegisterInteropMessageAssembly(typeof(IOrderContract).Assembly);
});These join the existing Amazon SQS interop options (which also picked up two bug fixes this week, #3190) and a fix to map Wolverine's TenantId from incoming MassTransit messages (#3192). The practical upshot: you can introduce Wolverine into an existing MassTransit or NServiceBus shop incrementally, service by service, over infrastructure both sides already trust.
๐ Interop with NServiceBus over database transports ยท ๐ Interop with MassTransit over database transports
๐ญ Observability & health โ
A large share of the week's Wolverine work exists to make running systems legible โ much of it surfaced directly through CritterWatch:
- A shared
BackgroundReceiveLoopwith receive-loop health reporting, now adopted across SQS, Redis, the PostgreSQL queue, the SQL Server queue, and Kafka (#3236). - Transport connection state surfaced in
EndpointHealthSnapshot, with a newIReportConnectionStateimplemented for NATS, MQTT, Pulsar, and Redis (#3231), plus a force-restart path for stuck listeners (#3232). - A sanitized, credential-free broker connection summary on
BrokerDescription(#3272) โ so the dashboard can show you where a broker points without ever leaking secrets. - Richer metrics: every instrument tagged with a
sourceservice name (#3221), dimensional inbox/outbox/scheduled gauges, and configurable histogram buckets (#3224). - The discovered gRPC endpoint manifest is now exposed via a
ServiceCapabilitiesdescriptor source (#3268, #3266), and RabbitMQ sending endpoints are now properly named in health snapshots (#3273).
๐ Instrumentation and Metrics ยท ๐ Diagnostics
๐ Reliability fixes & Pulsar โ
6.14.0 also closed out a major Pulsar re-evaluation effort โ DLQ/retry precedence, initial subscription position, multi-topic and regex subscriptions, native per-message redelivery, acknowledgment-strategy choice, a Reader interface for bounded replay and non-durable hot-tail, a tiered retry-letter error policy, producer deduplication, and both JSON and Avro schema support with broker-side registration (#3194โ#3215).
Two of those are worth showing. Pulsar's defining feature is broker-side schema registration and compatibility checking โ now a single fluent call, with the message body still owned by Wolverine's serialization:
opts.PublishMessage<OrderPlaced>()
.ToPulsarTopic("persistent://public/default/orders")
.UseJsonSchema<OrderPlaced>(); // or UseAvroSchema<T>() for Avro on the wireAnd the new tiered retry-letter policy โ the Pulsar analogue of the Kafka transport's MoveToKafkaRetryTopic โ expresses native redelivery delays as a first-class, discoverable error policy:
// On failure: redeliver after 5s, then 30s, then 2m, then dead-letter.
opts.OnException<TransientException>()
.MoveToPulsarRetryTopic(5.Seconds(), 30.Seconds(), 2.Minutes());๐ Pulsar schema support ยท ๐ Tiered retry-letter policy ยท ๐ Producer deduplication
Plus targeted reliability fixes: a RabbitMQ agent that could latch Disconnected after a channel-only shutdown (#3187), stable node identity for storeless Solo hosts (#3189), and re-attaching the sender wire tap to recovered envelopes (#3276).
Polecat โ Making It More Robust โ
Polecat shipped three releases this week (4.5.2, 4.6.0, 4.7.0), and the through-line is hardening: fewer sharp edges, more parity with Marten's behavior, and a real document-metadata story.
๐ก๏ธ Robustness & correctness fixes โ
- Repopulate the natural-key lookup table on projection rebuild (#261) โ rebuilds no longer leave natural-key lookups stale (mirrored by the same fix in Marten, below).
Patch().Set()now honorsEnumStorage(#264) and supportsDateTime/DateTimeOffset/DateOnly/TimeOnly(#265).- Sequential GUIDs for auto-assigned document ids (#245) โ far friendlier to index locality than random GUIDs.
AsStringenum LINQ predicates honor theJsonNamingPolicy(#224), computed-column indexes are usable by the LINQ translator (#225), on-the-fly event-store schema creation andInitialDataseeding work on startup (#233), andIEventStore.Identitynow varies byStoreNameso multiple stores stay distinct (#208).
๐ Document metadata โ
A genuinely new capability area: opt-in document metadata, end to end โ mirroring Marten's metadata model so the two stores behave alike. Enable the columns you want with a fluent DSL (or attributes) (#251, #252):
opts.Schema.For<Order>().Metadata(m =>
{
m.LastModifiedBy.Enabled = true;
m.CorrelationId.Enabled = true;
m.CreatedAt.MapTo(x => x.CreatedDate); // project a column onto your own member
});Then read just the metadata for a row โ no document body deserialization โ via the new MetadataForAsync<T> API (#253):
DocumentMetadata metadata = await session.MetadataForAsync(order);
// metadata.Version, .LastModified, .LastModifiedBy, .CorrelationId, .CausationId, ...Rounding it out: an opt-in user_name (LastModifiedBy) event-metadata column (#248), auto-seeding of CorrelationId/CausationId from Activity.Current on session open (#250), and session-level Headers with SetHeader/GetHeader (#249).
๐ญ Observability & CritterWatch โ
- An opt-in
polecat.event.appendOpenTelemetry counter (#247) and runtime event-append observations viaIEventStoreInstrumentation.AppendObserver(#215). IDocumentStoreDiagnosticswith an enriched mapping descriptor (#210), structured partitioning in theDocumentMappingDescriptor(#214), and metadata capabilities + anIEventStorebridge with tenant-scoped document diagnostics (#254) โ the same descriptor surface Marten exposes, so CritterWatch sees Polecat stores the same way it sees Marten.
๐ Range partitioning โ
Declarative range partitioning for document tables (#257, #212), now with a Marten-parity fluent surface โ the classic time-series retention pattern:
// Marten manages the boundaries:
opts.Schema.For<MetricsSample>().PartitionOn(x => x.BucketEnd).ByRange(jan, feb, mar);
// Or let a DBA / pg_partman own SPLIT/SWITCH/DROP at runtime for retention:
opts.Schema.For<MetricsSample>().PartitionOn(x => x.BucketEnd).ByExternallyManagedRange(jan, feb);ByExternallyManagedRange(...) provisions the partitions once and then never reconciles them, so a later schema apply won't clobber the months your retention job has been splitting and dropping.
๐ Wolverine + Polecat integration guide ยท ๐ฆ Polecat on GitHub
Marten โ
Three releases (9.10.0, 9.11.0, 9.12.0), with a mix of concurrency-hardening, new partitioning options, and โ again โ observability work feeding CritterWatch.
๐ Concurrency & correctness โ
Close the
mt_events_sequencegap on concurrent Quick OCC failures (#4771) โ a first contribution from @KMDjkb. Under truly concurrentFetchForWriting+ Quick-append writes to the same stream, a losing transaction could burn a sequence value it never rolled back, leaving a permanent gap that stalls the async daemon's high-water mark. A new opt-in option takes aFOR UPDATElock in the OCC path so the loser blocks and raises a clean concurrency error before consuming a sequence value โ no schema migration required:csharpopts.Events.UseExclusiveLockOnConcurrentAppends = true;Fix a false
ConcurrencyExceptionfrom non-RETURNINGevent ops in a batchedSaveChanges(#4784).Repopulate
mt_natural_keyon projection rebuild (#4793) โ the Marten side of the same natural-key fix that landed in Polecat.
๐ Partitioning & queries โ
- Range-partition a document table by a non-tenant date column (#4780) โ the
PartitionOn(member, cfg)API already existed; a Weasel 9.3.0 fix makes the date-keyed retention path stable across deployments and time zones (partition bounds are now compared by normalized instant rather than raw SQL literal, so migrations no longer report a spurious destructive rebuild). - Metadata-filtered document and event queries (#4792) โ the diagnostics surface can now filter documents and events by
correlation_id/causation_id/last_modified_by, honored only when the store actually captures that metadata column.
๐ Document storage & date range partitioning ยท ๐ Document & event metadata
๐ญ Observability & CritterWatch โ
IDocumentStoreDiagnosticswith an enriched mapping descriptor (#4776) and populated event/document metadata capabilities with tenant-scoped document diagnostics (#4790).- Runtime event-append observations via
IEventStoreInstrumentation(#4783) and an exact-identityDeleteProjectionProgressByShardNameAsyncfor surgical projection-progress management (#4786).
The Common Thread โ
Three themes ran through all nine releases this week:
- Database-backed messaging matured โ Wolverine's PostgreSQL and SQL Server queues got faster, and now interoperate directly with MassTransit and NServiceBus over the same databases.
- Polecat got tougher โ a stack of correctness fixes, sequential GUIDs, a full document-metadata story, and range partitioning.
- Everything got more observable โ diagnostics descriptors, instrumentation hooks, OpenTelemetry counters, connection-state reporting, and credential-safe broker summaries across Wolverine, Marten, and Polecat โ all converging on a single, consistent surface for CritterWatch to manage.
As always: upgrade, and tell us what breaks. This week's patch cadence is the proof that we listen.

