
Right before heading into a short vacation, I wanted to blog about some of our recent releases this past week as the entire CritterStack has been busy lately. 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 Beta 1
The big win for the week is (finally) getting out the first CritterWatch 1.0 beta, which I finally managed to present in a live stream yesterday. And while there’s a lot further to go for a true, quality 1.0 release, I think it’s showing a lot of promise and will add quite a bit of value for Critter Stack users at both development and production time.
There’s also several new sample solutions at https://github.com/JasperFx/CritterStackSamples/tree/main/critterwatch that show little fake systems stood up with CritterWatch and Aspire using several permutations of Rabbit MQ, AWS SQS, Azure Service Bus, SQL Server, and PostgreSQL — including the new embedded model.
Wolverine
Three releases this week, but the headline is clear: database-backed messaging got faster, and Wolverine now has more options for interoperability with NServiceBus and MassTransit. Both of these improvements were client requests for JasperFx Software.
🚀 Database queue performance
Both the PostgreSQL and SQL Server transports got performance improvements, with the SQL Server work being quite a bit more important, but with a new opt in option so that existing users won’t be surprised by database migrations.
On SQL Server the new optimization 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
Wolverine already has quite a few options for interoperability with pre-canned recipes for both NServiceBus and MassTransit against all the major message brokers, but we had a client request to do the same with NServiceBus and SQL Server, so we just beefed up all the permutations while we had the hood up. 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
Okay, so big parts of this are AI written and you don’t care much about the details. Just take my word for it that all this mumbo jumbo means that CritterWatch can “see” and report back to you much more about how your system is running, what your system actually is, and we’ve added more robustness to monitoring and kick starting external transport listeners.
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
We did a big round of improvements for Kafka a couple weeks ago to open up more Kafka idioms to Wolverine users. Later though, we did the exact same thing for Wolverine’s Pulsar support.
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 wire
And 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 is finally getting some serious people using it, and that has inevitably meant that more issues are arising. While the Critter Stack team can certainly not claim to be perfect in our delivery, I’ll swear up and down that we’re the most responsive team of maintainers in .NET and we’ve been turning around Polecat issues fast to get that thing as robust as possible for our early users. Polecat is also moving pretty fast because I’m making a big deal of ensuring that all CritterWatch features for Event Sourcing or the Document Database features are fully supported for Polecat, and that’s generated a lot of recent work in Polecat as well.
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
I found this gap during CritterWatch development:(
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):
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
This came from CritterWatch integration as well.
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:opts.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.










