Skip to content

Critter Stack in Review: October 12th to 30th

Jeremy Miller30th October 2026
Critter StackAlbaWolverineMartenPolecatFisherWeaselBobcatCommunity
The Jasper sign

This is a round up of everything that shipped across the JasperFx GitHub organization since the last review on October 12th. It's grouped by theme rather than by tool this time. The Critter Stack continues to receive a lot of community contribution and the people who sent us code and bug reports come first.

I said last time that the pace wasn't sustainable, and yet here we are and things haven't slowed down:

Oct 12 to Oct 30
Tagged releases across Wolverine, Marten, Polecat, Fisher, Weasel, and the shared JasperFx libraries72
Plus Bobcat 1.0.0 and Alba 9.0.0 RC32
Pull requests merged across the public repositories464
Pull requests from people outside JasperFx57, from 18 contributors
Issues opened by people outside JasperFx88, from 45 different reporters

A few things from this window already got their own write up, so I'm going to point at those rather than repeat them: the October release wave for CritterWatch 1.1, Bobcat 1.0, AI Skills 1.17, and the first look at Stoat; dead letter failure groups; broadcasting a message to every node; the serverless transactional outbox; rebuilding projections without downtime with warm-up nodes; agent assignment at fleet scale; and the rewritten Wolverine best practices guide.

What's left is still a lot, and it sorts into five themes:

  1. Not losing messages. A durable inbox and outbox hardening pass across Wolverine 6.51 through 6.58 that closed, by my count, a dozen distinct ways a message could go missing or run twice. Nearly every one of them came in as a community report with a reproduction.
  2. Performance, from SQL Server statements that were scanning the whole envelope table, to the run command holding a thread pool thread hostage for the life of your container, to a leader heartbeat that was describing every projection in every store every ten seconds.
  3. Shutting down cleanly. One contributor ran Wolverine through an orderly shutdown and filed nine issues about what it logged and what it leaked. We fixed all nine. As I said on DotNetRocks a couple weeks ago, stopping a running system with work in flight has been one of the biggest technical challenges over time and one of my favorite examples of "no, you can't just vibe code this over a weekend."
  4. More resilient async daemons. Transient failure classification for every store, paused shards that restart on their own, rebuilds that resume from where they died, and projection start floors.
  5. Multi-tenancy at scale, mostly Marko Lahma's doing, in the per-tenant partitioned events corner of Marten that was purpose built for massive systems and has admittedly been more complicated than we'd hoped upfront.

Alba 9, separately ​

Alba

Alba 9.0.0 RC3 shipped on the 25th, the last release candidate before 9.0 goes stable, and it's the biggest release Alba has had in years: host pooling for parallel integration tests, scenarios addressed by route name, JSON path and problem details assertions, endpoint coverage and authorization audits, time travel, and a rebuilt docs site. All of it is Jaedyn's work, and it deserves more than a section in a roundup, so Alba 9 gets a post of its own this Sunday. The short version until then: if you're on .NET 10, the RC3 packages are on NuGet, and the v9 changelog has the breaking changes to read first.

Thank you ​

Eighteen people outside JasperFx had pull requests merged in these three weeks. Fifty seven pull requests is the most we've ever had from the community in a window this size, and the quality is what gets me. These are not typo fixes.

Marko LahmaTwenty four pull requests, which is a record for anybody not on the payroll. The bulk of it is Marten's UseTenantPartitionedEvents mode, which is covered in its own section below: per-tenant subscription rewind, a removed and re-added tenant no longer stuck at its old high water mark, DCB tag rows deleted before a tenant's partition is dropped, per-tenant subscription agents seeded from their starting position, and sharded tenancy resolving a shard's database id instead of registering it as a tenant. On the Wolverine side, a stable envelope id for Kafka records from non-Wolverine producers so the durable inbox can deduplicate a redelivery, then the partitioned inbox refusing a redelivery it already handled. In Weasel, finishing a half-built concurrent partitioned index instead of failing with 42P07 forever. And two Marten performance fixes: a start-only commit no longer queries for a snapshot that cannot exist, and LoadManyAsync with no ids skips the database.
John KaramSix pull requests across Wolverine.HTTP and Marten, each paired with the issue that explains it. A string response declares its UTF-8 byte count as Content-Length, so non-ASCII text no longer gets the connection aborted by Kestrel. Array and list query parameters whose [FromQuery] name isn't a C# identifier, like tag-ids. A repeated request header binds the comma-joined values instead of a 500. Actions chained after ScheduleRetryIndefinitely() actually apply. Then in Marten, IsNullOrWhiteSpace() matching tabs and line breaks and EqualsIgnoreCase() escaping LIKE wildcards.
James (Clay) DavisOpt-in LISTEN/NOTIFY wakeups for PostgreSQL queues, which took the median delivery time on an idle queue from about 650 milliseconds to under 3. PostgreSQL durable pops that stay recoverable after a read failure, serverless startup with RavenDB persistence, and container-registered services treated as endpoint dependencies rather than request bodies.
Anne Erdtsieck-WijnenA shard the daemon paused on a transient failure is restarted with a capped backoff instead of staying paused until somebody restarts the pods, which is the fix that kicked off the whole daemon resiliency theme below. Blue-only partitions no longer ping-pong between blue nodes during a warm-up, which was 1,000 to 3,000 reassignment commands a minute in production. Partition child index names kept inside PostgreSQL's 63 character limit. And a steady stream of design issues worth reading on their own: DoThenRetry(), which shipped in 6.52.0, application-registered exception transforms, which shipped in Marten 9.52.0, and cold storage for finished event streams, which hasn't shipped yet and is going to take some thinking.
Jaedyn (Hawxy)All of Alba 9.0.0 RC3, which gets its own post this Sunday: pooled host support, resolving a route by name plus HTTP OPTIONS, a pile of new extensions including endpoint coverage and time travel, and an Aspire example. Alba has been Jaedyn's for a while now, and 9.0 is the proof.
vpetrevskiA per-event Quick append in Weasel can read back the sequence it drew, and then Marten uses it so a failed batch tombstones the number. Before this, a stream start that failed after its INSERT left a hole in the event sequence that never closed. Also a sharp follow-up on stream id collisions under archived stream partitioning.
Marc PetersFound that a Wolverine service whose schema was named order or user would start once and never again. Quoted reserved-word schema names in Wolverine's SQL, then went to the root in Weasel: index readback in a reserved-word or mixed-case schema, which is Weasel 9.47.1.
Tom LonghurstA leadership lock cancelled by node shutdown is no longer logged as an error, and the nine issues behind the "shutting down cleanly" section below. If you know Tom from TUnit, you already know the level of detail.
Tore HammervollReflection-free DCB tag type registration for Native AOT, with an issue that came with native runs and three costed alternatives. RegisterTagType<TTag, TInner>(unwrapper) exists in Marten, Polecat, and Fisher because of it.
Aksel KvitbergAn opt-in transaction-scoped durability pass lock for PostgreSQL, so ServerlessWithOutbox works behind a transaction-mode pooler like PgBouncer or Supabase's. Reported, designed, and written by the same person.
Kenneth SkovhedeThe source generator keeps a required member's initializer when it constructs a missing aggregate, which used to null every one of them.
Christian Bergum BergersenStreamPagedByCursor writes the page's bytes directly and disposes its pooled stream instead of re-encoding them.
Alper ÇelikCulture-invariant document alias casing. If you run Marten under a Turkish culture, read the upgrade note at the bottom before you take 9.55.0.
ahyousifManaged EF Core tenancy with WithDbContextAbstraction reuses the tenant DbContext for repository writes, the save, and outbox enrollment, where before it could fail code generation.
Martin René AndersenFound that circuit breaker restarts leaked RabbitMQ consumers that acked messages nobody handled. A later pause now supersedes an earlier pause's restart.
Deandre ThomasThe Azure Service Bus response queue keeps a stable identity during startup, where before a node number change mid-startup meant the queue never got created. First contribution.
Robert (thechucklingatom)CosmosDB sagas start with a conditional create instead of an upsert, so a second start message for an existing saga throws SagaConcurrencyException like every other provider instead of silently overwriting a running saga.
Felix WinterhalterAn Include() joined on the queried document's own id keeps that id in its temp table when the document has duplicated fields. Pulling on that one found two more defects in the same corner.

And the reports that were good enough to fix from, which in this window is a long list because the durability section below is mostly made of them:

Fifty seven pull requests from eighteen people, eighty eight issues from forty five. Thank you, all of you.

Not losing messages ​

This is the theme I care about most, and it's the one where the community did the most. Wolverine's durable inbox and outbox are the whole reason to use durable messaging, and over about two weeks we got a run of reports, nearly all with reproductions, describing ways that a message could be lost, handled twice, or stuck. We built a new durable receiver compliance rig on Bobcat, ran it across PostgreSQL, SQL Server, Marten, and EF Core over local, database, RabbitMQ, and SQS endpoints, and it found the rest. Most of this is Wolverine 6.51.3, with the follow-ups in 6.57.0 and 6.58.0. None of it changes an API, and the one schema addition, a nullable envelope_id column on the deduplication table, is optional.

Messages that were being lost:

  • The HTTP transport answered 200 while dropping the batch (6.58.0). When a durable local queue couldn't write an incoming batch to its inbox, the batch endpoint still answered 200, so the sender's durable outbox deleted messages that were never stored on either side. It answers 503 now and the sender retries. Derek Flenniken's report. The durable TCP listener had the same shape, answering Received for a batch its inbox couldn't store, and now answers ProcessingFailure.
  • A CloudEvents endpoint counted a rejected send as delivered (6.56.0), so a durable endpoint aimed at /_wolverine/invoke could delete an outgoing envelope the receiver never accepted.
  • A failed dead letter write was discarded silently (6.51.3) while the envelope was reported as moved to the error queue. The durable receiver awaits the write now, and a failed write is never reported as dead lettered. Same family: a disposed message store refuses a dead letter instead of dropping it.
  • Outgoing envelopes a transport couldn't map were logged and deleted (6.52.0). On Azure Service Bus, SNS, and Kafka they're dead lettered now instead of being reported as sent.
  • A durable batch's scheduled retry was lost (6.51.3). It's held in memory now. Patrick Koerkamp's report.
  • Messages lost after a RabbitMQ outage (6.51.3). Ahmet Ayhan worked out two mechanisms: two copies of one envelope in a single batch tripped the tenant write brake and paused the listener, and listener rejects exhausted a quorum queue's delivery limit. Duplicates in a batch are now reported as DuplicateIncomingEnvelopeException on every relational store, DeferAsync publishes before it acks, and there's an opt-in x-delivery-limit for durable quorum queues with a startup warning if you haven't set one. The 6.51.3 release notes have the detail, and there's a new "Quorum Queues and the Durable Inbox" section headed for the RabbitMQ docs page.

Messages that were being handled twice:

  • Inbox recovery reassigned rows without claiming them (6.51.1), so two nodes could recover the same envelope. Recovery claims are atomic and deadlock-free on every store now.
  • An envelope sent again from a Marten outbox after the receiver's handled row had expired ran a second time (6.51.3). Every relational queue sender now moves the outbox row atomically for every durable send: PostgreSQL, SQL Server, MySQL, Oracle, and SQLite each got their own pull request. Three of these were dbelinsky-accureference's.
  • A requeued failure was dead lettered as a duplicate of itself (6.51.3). Eager idempotency now writes its placeholder inside the handler's transaction on Marten, EF Core, Polecat, and Fisher.
  • Logical deduplication on an EF Core chain committed the claim before the transaction (6.51.3), so a crash in between left a claim with no work behind it. The claim rides the EF Core transaction now. Marinela Marchitan's report.
  • A [Deduplicated] claim stranded by a crash could never be retried (6.51.3). The same envelope can take the claim over now, which is the optional envelope_id column.
  • Messages from non-Wolverine producers were handled twice on redelivery (6.57.0). A Kafka record, a NATS JetStream message, a Pulsar message, a Redis Streams entry, or an SQS or Pub/Sub pull subscription message from something that isn't Wolverine has no Wolverine id, so the durable inbox had nothing to deduplicate on. Marko Lahma's fix gives them a stable envelope id derived from the broker's own identity, Kafka's topic, partition, and offset for instance, and 6.58.0 closes the last gap for partitioned inboxes, where the kept Handled row sat in a different partition and didn't collide.
  • A long handler was started a second time because InboxStaleTime measured from when the message was first written rather than when a node claimed it (6.51.0). A node now keeps the clock running for messages it still holds, and scheduled rows are left alone.
  • A handled message was replayed on every restart (6.51.0) when a durable PostgreSQL queue had a DbContext enrolled in an ancillary store, or Fisher or Polecat had an AddMessageHandler handler committing an ancillary session. The handled mark is written in the store that holds the inbox row now. stigrune-hoc's report.

Messages that were getting stuck:

  • A failed or lost durable database queue pop stranded envelopes on a live node (6.51.0), on PostgreSQL by Clay Davis's fix and then SQL Server, MySQL, and Oracle. dbelinsky-accureference's report.
  • A durable SQS listener never settled its inbox row (6.51.3) when the broker delivery was already acked, which had been true since 6.40.0. kowalczb's report.
  • An unresolvable tenant paused the listener for every tenant (6.55.0). It's treated as tenant-scoped on the single-envelope inbox write now. billshuck's report.
  • A Kafka listener with a durable inbox crashed the process with a stack overflow when the inbox database was unavailable (6.56.0), because DeferAsync called back into the receiver inline and the failed write and the defer recursed. A deferred Kafka envelope goes back on its own task after a short pause now, and because Kafka has no native requeue, a Requeue policy on a Kafka endpoint redelivers after about a second instead of immediately. SERAYU27112's report, and I'm glad it was a report and not a page.

And the one I'd most like you to know about if you run serverless: Aksel Kvitberg's Durability.UseTransactionScopedDurabilityPassLock (6.58.0) makes the ServerlessWithOutbox durability pass take a transaction-scoped advisory lock instead of a session lock, which is the only kind that's safe behind PgBouncer or Supabase's pooler. It's opt-in and documented on the serverless guide.

Performance ​

SQL Server envelope statements seek on their indexes again (6.59.1, today). Daniel Weinand found that several of the SQL Server message store's per-envelope statements were scanning the whole envelope table, and those run once per message. Weasel's generic command builder had no mapping for Guid, so every envelope id went out as sql_variant, and SQL Server had to CONVERT_IMPLICIT the primary key on every row. Envelope addresses went out as nvarchar against varchar columns, which under a SQL collation, the Azure SQL default, defeats the index too. And DateTime parameters were typed as Date, so they kept only the date. All three are fixed in the message store, and the new test reads SQL Server's plan cache and fails if any plan contains sql_variant or converts an indexed column. The Weasel mappings underneath are tracked separately, but Wolverine doesn't wait on them. If you're on SQL Server, take 6.59.1. It's a package bump only.

The run command no longer holds a thread pool thread for the life of your application (JasperFx 2.96.2, also today). This one is mildly embarrassing. run waited for shutdown by blocking a thread pool thread. On a container with a CPU limit under one core, ProcessorCount is 1, the pool's minimum is one thread, and run had it. Every burst of work waited about 500 milliseconds for the pool to add a thread. tzdevelopteam saw a 200 millisecond Task.Delay loop wake up nearly a second late on Azure Container Apps. It awaits now, and RunJasperFxCommands() returns a task when the host is already started. This applies to every Critter Stack application, whatever the persistence or transport.

Roslyn metadata references are shared across compilations (JasperFx 2.96.1). Bhanu Prakash Korthiwada measured 60 to 100 MB of native memory per runtime code generation, freed only by finalizers, because every compilation rebuilt a MetadataReference for every reachable assembly. They're cached now. If you've ever watched a test suite's memory climb while Wolverine generated handlers, this is part of why.

Two memory leaks in test suites. Wolverine's runtime never disposed its Meter (6.50.1), and the durable persistence gauges on it hold callbacks back into the runtime, so every disposed host stayed reachable for the life of the process. AndriiKononenko3 traced the retention path. And stopped subscription agents never released their heartbeat timer (JasperFx 2.90.2), which is a substantial leak if you constantly start and tear down IHost instances in testing. We do, which is how we noticed.

The leader's heartbeat no longer describes every projection in every store every ten seconds (6.50.0, plus Marten 9.50.1, Polecat 5.40.1, and Fisher 1.20.2). Assigning event subscription agents only needs each store's shard names and its database topology. It was building the full usage descriptor, which also read the highest event sequence and described every projection, subscription, event type, and tag type, for every store, on every heartbeat. Laurence Gillian's report came with the heartbeat math already done, and the same report fixed Marten logging a PostgreSQL ERROR every ten seconds for a store with no event types. JasperFx 2.87.1 added the narrower TryDescribeDatabasesAsync() and the three stores answer it straight from their tenancy.

LISTEN/NOTIFY for PostgreSQL queues (6.50.0, Clay Davis). Opt in with EnableQueueNotifications() and an idle queue listener is woken as soon as a message is written instead of waiting out its polling interval, 650 milliseconds median down to under 3 in local benchmarks. It's off by default because it holds one extra long-lived connection per database per process. Read the trade-offs before you turn it on.

One slow tenant no longer holds back every tenant's sends (6.52.0). The durable outbox's delete coalescer was shared across tenant databases, so under database-per-tenant one slow database stalled everybody. Stores and deletes are coalesced per tenant database now. Ahmet Ayhan's report.

Timer agents for short-cadence recurring schedules (6.52.0). A recurring schedule that fires more often than every 15 minutes now runs on a timer agent, one per schedule, instead of round-tripping through the scheduled message store every time. Same deduplication id as the scheduled path, pause and resume through IRecurringScheduleControl. This is a behavior change; opt a schedule out with UseScheduledMessages().

Marten stops doing work that can't return anything. Marko Lahma's start-only commit fix (9.50.3): an inline single-stream projection asks storage only for the ids of streams being appended to, so a commit that only starts streams passed an empty id array and still paid a round trip on id = ANY($1), plus a second connection in the default session mode. The same guard removes a wasted round trip from an async daemon batch whose slices all hit the aggregate write cache. His LoadManyAsync fix (9.51.0) is the same idea. Christian Bergum Bergersen's StreamPagedByCursor fix (9.55.0) stops re-encoding every page. And compiled queries now plan from a generated template and refuse values computed from the query object (9.51.0), which closes a class of "the compiled query worked once" bugs.

Agent assignment stopped churning during warm-ups (6.50.0). Anne Erdtsieck-Wijnen's ping-pong fix is covered in the agent assignment post, but it belongs here too: 1,000 to 3,000 ReassignAgent commands a minute down to a partition that stays put unless another node is strictly lighter.

Shutting down cleanly ​

Tom Longhurst, who you may know as the author of TUnit, ran Wolverine through orderly shutdowns on Azure Service Bus and filed nine issues in two days about what it logged, what it leaked, and what it waited on. Every one is fixed in 6.55.0 through 6.59.0:

  • A database batch cancelled by Wolverine's own shutdown was wrapped in DatabaseBatchCommandException and logged as an error. Genuine failures still are; a shutdown cancellation isn't.
  • A send that IHost.StopAsync() cancelled mid-flight was logged as a send failure, twice. A durable agent logs it at Information now, because the envelopes are in the outbox and come back on restart. A non-durable agent logs it at Warning, because those messages really were not sent. Neither counts toward the circuit breaker.
  • A leadership lock acquisition cancelled by shutdown was logged as an error. Tom's own pull request.
  • The durability metrics sweeper outlived host shutdown and threw ObjectDisposedException against a disposed NpgsqlDataSource. Shutdown stops the sweeper and waits for any query in flight now, and removing a durability agent waits for its query too.
  • DurabilityAgent.StopAsync() didn't join its in-flight command, so a command still running at shutdown could outlive the host and fail against a disposed data source. It cancels and waits now.
  • Azure Service Bus session listeners disposed their CancellationTokenSource before the receive loop stopped. They join the loop first now.
  • Stopping a listener waited out an inline retry cooldown. It interrupts it now, and the retry doesn't run after the stop.
  • Shutdown didn't wait for handlers still running on a buffered local queue. It does now.
  • And two Windows CI findings that were ours to fix, not yours.

Related, from other reporters: Fausto Davila found the SQLite listener cancelling its polling tasks without observing their completion (6.51.2), and database queue listeners now keep a deferral across a stop. Marko Lahma stopped Wolverine disposing a DbDataSource it didn't own, and kept a blocking receive loop on its dedicated thread past its first await.

If your Kubernetes termination grace period has been catching Wolverine mid-sentence, this is the release line to move to.

More resilient async daemons ​

Anne Erdtsieck-Wijnen's fix for a paused shard that stayed paused until the pods restarted needed one thing the daemon didn't have: a way to tell a failure that will clear on its own from one that won't. That became a small chain of releases across JasperFx and all three stores.

  • ShardFailure.IsTransient (JasperFx 2.89.0) classifies a shard's failure from its exception graph: a DbException that says it's transient, a timeout, a socket error, a non-file-system IOException. Npgsql classifies correctly on its own. Microsoft.Data.SqlClient and Microsoft.Data.Sqlite don't override IsTransient at all, so Polecat 5.42.0 added a SQL Server classifier (timeouts, deadlocks, Azure SQL throttling and failover) and Fisher 1.21.0 added a SQLite one (SQLITE_BUSY, SQLITE_LOCKED, SQLITE_PROTOCOL).
  • The daemon's retry decision consults those classifiers (JasperFx 2.90.1, Polecat 5.42.1, Fisher 1.21.1). Before this, a SQL Server deadlock or a SQLITE_BUSY thrown from projection code was a poison event on the first throw, and under SkipApplyErrors it was dead lettered even though it would have applied on retry. Now the shard pauses, the coordinator restarts it, and nothing is dead lettered.
  • A systemic database failure pauses the shard instead of being dead lettered event by event (JasperFx 2.93.0, Polecat 5.44.0, Fisher 1.22.0, Marten 9.52.0). A missing column or a permissions error would fail every following event the same way, so dead lettering them one at a time was just noise. This is a behavior change under SkipApplyErrors, and Marten now attributes the failure to the event that hit it instead of reporting Other with no event.
  • A failed rebuild resumes from its committed progress (JasperFx 2.88.0). Every position strategy used to answer Rebuild mode with a rewind to zero, so a restarted rebuild replayed its history over documents it had already built. It starts from the shard's own persisted progress now, proven in all three stores. And a rebuild that throws hands its shard back to continuous execution (Wolverine 6.50.0), where before it left the shard stopped for good. The health check sweep also leaves a shard alone while its projection is being rebuilt, through the new IProjectionDaemon.IsRebuilding(), so it can't start a second copy of the shard on top of a running rebuild (JasperFx 2.86.0).
  • The high water agent survives a failed watchdog restart (JasperFx 2.91.0) and restarts on a stalled mark. Jente Vets and Timo Vahtera both found it stopping permanently when the restart itself failed during a database outage. Marten 9.51.0 pins that it survives an outage that hangs connections, and adds AddMartenHighWaterHealthCheck<TStore>() for stores registered with AddMartenStore, which Jente also asked for.
  • Projection start floors (JasperFx 2.93.0, Marten 9.52.0, Polecat 5.44.0, Fisher 1.22.0). A rebuild now honors a configured SubscribeFromSequence or SubscribeFromTime instead of replaying from zero, and the new AsyncOptions.SubscribeFromFirstRun() pins the high water mark as a projection's floor on its first start and persists it. Marten adds a mt_event_projection_floors table on its next migration. Polecat and Fisher make it opt-in with EnableProjectionFloors = true, because it's a new table, and Polecat plans to flip the default in 6.0. Polecat docs.
  • Preview and advance the high water mark, store agnostically (JasperFx 2.92.0). PreviewHighWaterAdvanceAsync shows the stored mark, the committed ceiling, and how many sequence numbers would be skipped. AdvanceHighWaterMarkToLatestAsync is a compare-and-set against what the operator was shown, and a mark never moves backwards. This is what CritterWatch's "jump-start a daemon held behind a gap" action calls, and it works on SQL Server and SQLite stores now, not just Marten.
  • Polecat's stale gap detection actually ages gaps (5.43.0). The age of a gap was read from the high water mark row's last_updated, which every detection cycle re-stamped, so a gap left by a rolled-back append was never skipped while a daemon ran and CatchUpAsync could hang for its full five minutes. It ages from when the detector first saw the mark held there, as Marten does.
  • Polecat's extended progression tracking is now actually written (5.41.0). With EnableExtendedProgressionTracking on, including through CritterWatch, a running daemon never wrote the heartbeat, agent status, pause reason, running node, or failure columns. It does now. I'd like to tell you we found that one ourselves.

Multi-tenancy at scale ​

Marten's UseTenantPartitionedEvents mode, one event partition per tenant, had never been pushed as hard as Marko Lahma pushed it this month. Across Marten 9.51.0, 9.52.0, and JasperFx 2.91.0:

  • Subscriptions rewind per tenant. A tenant-less rewind under per-tenant partitioning refuses a non-zero floor rather than silently doing the wrong thing.
  • A removed and re-added tenant is no longer held at its old per-tenant high water mark, and a re-added tenant's agent started before its first append projects every event.
  • Removing a tenant no longer fails with 23503, whether the store has DCB tag rows or a [NaturalKey] aggregate. The tag rows and natural key rows are deleted before the partition drop.
  • Per-tenant subscription agents seed their progress from their starting position, an empty tenant list comes back before the first tenant is registered instead of 42P01, and MarkEventsAsSkipped is scoped to a tenant.
  • Sharded tenancy resolves a shard's database id instead of registering the shard as a tenant, and master-table and sharded tenancy let the tenancy's own AutoCreate pick the apply mode.
  • Per-tenant timestamp rewinds in Polecat resolve each tenant's own floor (5.43.0). Every one of them used to throw.

Fisher doesn't need any of the per-tenant pieces, because one SQLite file has one event sequence for every tenant in it, and database-per-tenant runs one daemon per file. It did get an opt-in Wolverine-managed event subscription distribution in 6.51.2, so a Fisher-backed fleet can spread its subscriptions the way a Marten one does.

On the Wolverine side, the per-tenant outbox coalescing and the unresolvable-tenant listener fix are above. And Anne Erdtsieck-Wijnen filed a memory pressure issue from managed event subscription distribution at tenants times projections scale, about 52,000 agents with gen2-dominated OOMs during warm-up. That's still open, and the interim advice is in the docs: set MaximumHopperSize to about twice BatchSize. Nobody had run this at 52,000 agents before. We're learning what that looks like in cooperation with a JasperFx client, and the fixes will keep coming.

Marten, the rest of it ​

Marten

Eleven Marten releases, 9.50.0 through 9.55.0. Beyond the themes above:

TombstoneStream, a hard stream delete (9.50.3). session.Events.TombstoneStream(id) queues a single-statement delete of a stream's tag rows, its events, and its stream row, scoped to the session tenant under conjoined tenancy, and it runs at SaveChangesAsync like every other write. The event sequence is never touched and projected documents are deliberately left alone. It's on the shared IEventStoreOperations contract with a compliance suite, so Polecat (5.42.0) and Fisher (1.21.0) have the same thing.

DCB under Native AOT (9.50.3). Tore Hammervoll's RegisterTagType<TTag, TInner>(unwrapper) names the inner type so the generic closes at compile time, and the inferring overloads now carry [RequiresUnreferencedCode]. Because an analyzer warning never reaches a store configured from inside a library, a runtime failure now throws naming the exact line to write. With explicit registrations, tagging, tag rules, EventTagQuery, and FetchForWritingByTags all run from a native binary, and a CI job publishes and runs them. Same API in Polecat and Fisher.

Three Include() defects in one corner (9.50.0). Felix Winterhalter's fix for an Include() joined on the queried document's own id in a query session led to a second 42703 when the query also called Stats(), and then to TotalResults reporting the page size instead of the match count for Stats() plus Include() plus Select() plus Take(). That last one is a value change, to the correct value, so if you compute page counts from it, your counts change.

Application-registered exception transforms (9.52.0, Anne's design). StoreOptions.Advanced.ExceptionTransforms lets you turn a PostgresException with your own SQLSTATE into a typed exception, consulted before Marten's built-ins.

Fixes worth knowing: a cancelled save rolls back and writes its tombstones off the failed save's path (9.51.0), where before it left its transaction open and the inline tombstone write waited on it for the full command timeout. HiLo no longer caches a failed factory build across an outage (9.55.0). Subclass queries use the duplicated column for nested members (9.55.0). A multi-stream projection's revision is no longer truncated through int (9.52.0), so ILongVersioned works as advertised. UndoDeleteWhere and HardDeleteWhere no longer cause a false ConcurrencyException later in the same session (9.52.0). A custom foreign key name survives conjoined tenancy (9.50.3), which is what made overlong foreign key names unfixable. Archiving a tagged stream under archived-stream partitioning no longer fails with a foreign key violation (9.50.3). And an inline array literal in a LINQ Contains() is treated as a value, including a compiled query's own members (9.51.0).

For CritterWatch's explorers (9.52.0, 9.53.0, and the same day in Polecat and Fisher): QueryEventData<T>() is store-agnostic LINQ over the bodies of one event type across every stream, PreviewDocumentJsonAsync is a dry run of a document save that refuses exactly what the save would refuse, UndeleteDocumentAsync restores a soft-deleted document, OrderByMetadata orders a document page newest first, and DocumentTypeRef.IsSoftDeleted tells a console whether a delete is permanent before it does it. All default interface members, all binary compatible.

Polecat and Fisher ​

Polecat    Fisher

Polecat shipped eleven releases, 5.40.0 through 5.46.1, and Fisher eight, 1.20.2 through 1.24.1. Most of both are the shared contract work above landing the same day as Marten, which is the whole point of the shared contract. Beyond that:

Polecat:

  • A failed or cancelled save ends its transaction at once (5.43.0), so it no longer holds a stream lock taken by FetchForExclusiveWriting until the session is disposed. Post-commit hooks get CancellationToken.None now, so a token cancelled after COMMIT no longer fails a committed save.
  • A save that starts several streams reports the stream that actually collided (5.43.0) in ExistingStreamIdCollisionException.
  • The document-side store subject is polecat://{storename} (5.44.0), as Marten and Fisher report, where it used to return the database URI. If a tool keys on IDocumentStoreDiagnostics.Subject, that's a change.
  • Two binary compatibility fixes (5.42.0). TombstoneStream and CompactStreamAsync<T> are declared on IEventOperations again. Both had moved up to JasperFx base interfaces, which compiles fine but throws MissingMethodException for already-compiled callers, because .NET doesn't look in base interfaces to resolve those. CompactStreamAsync<T> had been broken for code compiled against 5.10 or earlier since 5.11.0. Nobody noticed, which tells you something about how many people compact streams from a library.
  • Every content pull request in 5.43.0 was gated locally on the full supervised suite under Bobcat: 3,413 tests on SQL Server 2025 with four workers. That's the dogfooding I was hoping Bobcat would make possible.

Fisher:

  • ITracked documents carry the session's metadata (1.22.0). CorrelationId, CausationId, and LastModifiedBy map onto their own columns by convention, as Marten's TrackedPolicy does. An existing ITracked type gains three columns on its next migration.
  • An enlisted session's HiLo block reservation is refused instead of hanging (1.21.2), and a session's post-commit hooks run once, after the commit.
  • AdvanceHighWaterMarkToLatestAsync no longer lowers a mark that's above max(seq_id) (1.22.0). Use TryCorrectProgressInDatabaseAsync for that.
  • Fisher enrolls 62 of the 63 shared compliance suites now, 747 shared tests, with 2,795 tests green on net9.0 and net10.0. Fisher remains covered by JasperFx support plans.

Weasel ​

Weasel

Six releases, 9.45.0 through 9.47.1. Four of the headline items came from outside.

  • db-patch drop files leave untouched schemas alone (9.45.0). The drop file used to drop schema ... CASCADE for every schema the application knew about, so running it as a rollback destroyed schemas the patch never touched. It rolls back only what the patch changed now. This changes what an existing drop file does. Same release: primary keys can be added to and removed from existing tables, reported from Wolverine's SQL Server queue tables switching between a clustered key and a clustered IDENTITY.
  • A half-built concurrent partitioned index can be finished (9.46.0, Marko Lahma). Dropping a partitioned index removes only the children attached to it, and the child of the partition the run stopped at was never attached, so the rebuild asked for the same name again and failed with 42P07 on every run. Each partition's step now begins with a DROP INDEX CONCURRENTLY IF EXISTS for that child.
  • Advisory lock holders are read from the current database only (9.46.0). pg_locks is server-wide, so FindHolderAsync reported a lock as held by a neighbor database for a lock this node could have taken. Database-per-tenant deployments and parallel test lanes hit this by construction.
  • A foreign key is corrected for its linked key once, however many migrations run it (9.46.1, Marko Lahma). Parallel migrations over the same cached Table instances both saw the key one column short and both appended, so 42830, and the widened key then lived on the shared instance for every later migration in that process.
  • Partition child index names stay under 63 characters (9.46.1, Anne Erdtsieck-Wijnen). PostgreSQL truncated them into the suffix, which is the only part that distinguishes one partition's child from another's.
  • Indexes are read back in a schema that has to be quoted (9.47.1, Marc Peters). In a schema named order or user, or one with uppercase, no index was read back at all, so every index was Missing rather than Different, the patch was a bare create index, and every run after the first failed with 42P07. The primary key constraint's name wasn't read back either. One change fixes both.
  • A per-event Quick append reads back the sequence it drew (9.47.0, vpetrevski), the Weasel half of the Marten tombstone fix above. Opt-in per dialect, defaults to the old behavior.
  • No more trim warning from Weasel.Core (9.46.0, Marko Lahma), found through Quartz.NET's trim canary. And a Firebird catalog read that raced a rewrite is read again instead of reported.

Wolverine, the rest of it ​

Wolverine

Seventeen Wolverine releases, 6.50.0 through 6.59.1. What hasn't been covered above or in its own post:

Aspire parity: named connections everywhere (6.52.0). Every transport and every relational message store can be registered by connection string name now, so the Aspire-injected ConnectionStrings:postgres works the same way for Wolverine as it does for everything else in your app. NpgsqlDataSource and MySqlDataSource resolve from the container, static tenants can be named, describe has a Named Connection Strings table, and the docs lead with the named form. The Aspire samples moved to Aspire 13.6.3 with a shared ServiceDefaults project. Alba and Marten got Aspire examples the same week.

NServiceBus XML interop (6.57.0). Many NServiceBus systems still use its XmlSerializer, and they no longer have to switch to JSON before a migration to Wolverine can start. Every NServiceBus interop endpoint reads both JSON and XML, choosing per message from the NServiceBus.ContentType header, and NServiceBusXmlSerializer sends XML. Because one endpoint reads both, an NServiceBus estate can move from XML to JSON gradually. Also fixed: NServiceBus replies over RabbitMQ were going to a Wolverine system queue NServiceBus can't reach, so every reply landed in its error queue. Brad Jolicoeur asked for the XML. Docs.

[DeduplicatedWithResponse] claim-in-transaction (6.52.0, 6.54.0). Laurence Gillian's four issues, start to finish: ClaimInTransaction writes the claim and the stored response inside the endpoint's transaction, so a failure after the database commit can't release the claim and let the retry run the work again. There's an application-wide WolverineHttpOptions.DefaultClaimInTransaction, the gaps for [WriteAggregate] binding the aggregate rather than IEventStream<T> are closed, and a concurrent repeat that sees the first request's committed work answers the first response rather than the endpoint's own refusal. Two Marten commit-failure fixes rode along. Docs.

Scheduled messages know about sagas (6.59.0). ScheduledMessageSummary.SagaId and a SagaId filter on ScheduledMessageQuery, so you can query or cancel one saga's pending timeouts. No schema change. Daniel Winkler's ask, and the first step toward finding scheduled messages by a business identifier.

opts.UseHttpTransport() (6.56.0) registers the HTTP transport inside UseWolverine(), so http URIs resolve in ListenForMessagesFrom(), PublishMessage<T>().To(uri), and EndpointFor() without first calling ToHttpEndpoint(). And /_wolverine/invoke answers 415 instead of 500 for a content type with no serializer. A buffered HttpEndpoint aimed at /invoke sends each envelope on its own now (6.55.1) instead of getting a 415 for every batch and retrying forever.

Wolverine.HTTP, beyond John Karam's four: an omitted query, header, or form value binds the parameter's declared default (6.51.2). Single-Value strong-typed id wrappers like record struct OrderId(Guid Value) bind without a TryParse (6.50.0). HTTP handlers mapped directly use ASP.NET Core's JsonOptions without MapWolverineEndpoints(), and a single endpoint can have its own JSON options (6.51.0). Opt-in RejectUnparseableHeaderValues answers 400 with ProblemDetails for a [FromHeader] value that won't parse (6.57.0). And a container-registered service posted on a POST or PUT is resolved from the container on every endpoint (6.50.0, Clay Davis). That one's a behavior change: if you register a type in the container and also post it as a body without [FromBody], add [FromBody].

Error handling: DoThenRetry(action, maxAttempts) works both inline and from a queue (6.52.0, Anne's design). Additional actions chained after ScheduleRetryIndefinitely() or RequeueIndefinitely() apply, including to the open-ended attempts (6.57.0, John Karam). The failure log level is configurable per chain or globally (6.51.0). MessageCausedBy names the handler you wrote, not the generated class (6.55.0).

Operability: AssignmentGrid.Agent.UnplacedReason says why an agent isn't running, AwaitingCapacity, NoCapableNode, Paused, TakeoverHold, or Superseded, and the leader logs agents awaiting capacity (6.52.0). ServiceCapabilities reads each store on its own and reports FailedSections, so a failed section is distinguishable from an empty one. A header-equality filter on DeadLetterEnvelopeQuery is honored by bulk replay and discard across every store. The db-* commands discover the Oracle message store (Pritam Rao's report). The static handler registry keeps handlers your custom discovery rules select, a regression since 6.44.0 (Maksim Vorobev's report). CloudEvents carries lowercase Envelope.Headers as extension attributes. And EndpointName is a typed field on EndpointDescriptor now, so a monitoring tool reading the old properties row should move.

Getting ready for Wolverine 7.0 (6.52.0). I wrote about the .NET 11 wave on my own blog, and the change I most want in Wolverine 7 is one handler chain per handler type instead of combining every handler for a message type into one logical handler. 6.52.0 lays the groundwork: MultipleHandlerBehavior.ClassicCombineIntoOneLogicalHandler is [Obsolete] but still the default, there's a startup warning for combined handlers, an opt-in AssertNoCombinedHandlers() if you want to get ahead of it, a handler.type OpenTelemetry tag on every chain, and logical deduplication scoped per chain. A couple of latent Separated bugs got fixed along the way.

Security. CVE-2026-84008 is a high-severity denial of service in System.Security.Cryptography.Pkcs that arrived transitively through Microsoft.Data.SqlClient, Oracle.ManagedDataAccess.Core, and Polecat. Wolverine 6.50.1, Weasel 9.45.1, and Polecat 5.40.2 reference the patched version directly, and Wolverine 6.50.2 means a NuGet audit comes back clean whichever of them you reference. Marten was never affected. WolverineFx.SignalR also moved off a vulnerable SignalR.Common.

Bobcat and CritterWatch ​

Bobcat    CritterWatch

Bobcat 1.0.0 shipped on the 27th and got its own post. The thing to know from the Wolverine side is that WolverineFx.Bobcat and WolverineFx.Bobcat.EventModel are out of alpha as of 6.55.0 and version with Wolverine. WolverineFx.Bobcat.Http stays alpha until Alba 9 goes stable, because a stable package can't depend on an RC. The Critter Stack vocabulary moved out of Bobcat into WolverineFx.Bobcat at 1.0.0-rc.2, so Bobcat itself takes no JasperFx or Wolverine dependency; if you're coming from 0.x, read the compatibility page first. Every CI suite in Marten, Polecat, Fisher, Weasel, and Wolverine now runs through Bobcat's supervisor, and ./build.sh LocalGate in each repository runs the whole thing on one machine before a push.

CritterWatch 1.1 is in the release wave post. Everything in the "for CritterWatch's explorers" paragraph above exists so that its Document Store Explorer and Event Store Explorer work the same on Marten, Polecat, and Fisher.

Upgrading ​

The dependency line is Wolverine 6.59.1, Marten 9.55.0, Polecat 5.46.1, Fisher 1.24.1, and Weasel 9.47.1, on JasperFx 2.96.2, with Bobcat 1.0.0 and WolverineFx.Bobcat 6.59.1. Take them together. Alba is 9.0.0-rc.3 if you're on .NET 10 and want the new stuff now, and 8.x otherwise.

There are more behavior changes in this window than usual, so read these first:

  • Marten 9.55.0, if you run under a Turkish or Azerbaijani culture and have a document type with an uppercase I in its name. Aliases are lowered with the invariant culture now, so Invoice maps to mt_doc_invoice where it used to be mt_doc_ınvoice. Rename the existing tables and upsert functions before you upgrade or your documents will appear to be missing. Every other culture is unaffected.
  • Marten 9.55.0 changes StoredDocument.Version for projection documents using UseVersionFromMatchingStream with revisions. Anything parsing that token as an integer needs updating.
  • JasperFx 2.93.0, Marten 9.52.0, Polecat 5.44.0, Fisher 1.22.0: under SkipApplyErrors, a permanent database error from projection code pauses the shard instead of dead lettering every following event.
  • Marten 9.52.0 adds the mt_event_projection_floors table on the next migration. Polecat and Fisher only add theirs when you opt in.
  • Marten 9.50.3 shortens foreign key and index names over 63 characters with a hash suffix. If you raised NAMEDATALEN and have a name between 64 characters and your own limit, it'll show up in a migration diff. An empty DCB tag table suffix is refused.
  • Marten 9.50.0 changes TotalResults for Stats() plus Include() plus Select() plus Take(), from the page size to the match count.
  • Wolverine 6.54.0 migrates the dead letter table: seven nullable columns, an index, and a wolverine_dead_letter_history table, through the normal schema migration.
  • Wolverine 6.52.0 runs recurring schedules under 15 minutes on timer agents, and dead letters outgoing envelopes a transport can't map. Opt a schedule out with UseScheduledMessages().
  • Wolverine 6.50.0: a container-registered type posted as a request body needs [FromBody] now.
  • Weasel 9.45.0: db-patch drop files no longer drop schemas, and PrimaryKeyName is empty for a table without primary key columns unless you set it.
  • Weasel 9.46.0: AdvisoryLock.FindHolderAsync returns null where it used to name a holder in another database, and concurrent partitioned index DDL carries a DROP INDEX CONCURRENTLY IF EXISTS per partition.
  • Polecat 5.44.0: the document-side store subject is polecat://{storename} instead of the database URI.
  • Fisher 1.22.0: an ITracked type gains three columns, and Advanced.AdvanceHighWaterMarkToLatestAsync no longer lowers a mark.
  • If you're on SQL Server or Oracle anywhere in the stack, Wolverine 6.50.1, Weasel 9.45.1, and Polecat 5.40.2 are the floor for the Pkcs advisory.

Where this is going ​

.NET 11 lands on November 10th, and as I wrote on my own blog, I'm leaning toward a wave of major versions right behind it: Marten 10, Wolverine 7, Polecat 6, Fisher 2, Weasel 10, JasperFx 3. Few breaking changes, back ports to the current lines at least through January, and the one structural change I actually want, which is the one-chain-per-handler model that 6.52.0 started preparing for. The durability rig, the supervised CI suites, and the compliance suites across the three stores are what make me think we can do that wave without the three weeks of stabilization the last one took.

In the meantime, the fastest way to get a fix is still to file an issue with a reproduction. Look at the list above if you doubt it. And if you're running any of this at a scale that scares you, that's exactly what JasperFx support plans are for.

RSS Feed · All Rights Reserved.