<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex E</title>
    <description>The latest articles on DEV Community by Alex E (@__2d3e61e).</description>
    <link>https://dev.to/__2d3e61e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3972169%2Fdeba24d7-a7da-4581-9c60-30514773514c.png</url>
      <title>DEV Community: Alex E</title>
      <link>https://dev.to/__2d3e61e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__2d3e61e"/>
    <language>en</language>
    <item>
      <title>When the disk fills up: pressure and tenant isolation in Squirix preview.7</title>
      <dc:creator>Alex E</dc:creator>
      <pubDate>Sun, 26 Jul 2026 15:03:06 +0000</pubDate>
      <link>https://dev.to/__2d3e61e/when-the-disk-fills-up-pressure-and-tenant-isolation-in-squirix-preview7-3dpa</link>
      <guid>https://dev.to/__2d3e61e/when-the-disk-fills-up-pressure-and-tenant-isolation-in-squirix-preview7-3dpa</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/__2d3e61e/what-happens-after-a-write-reworking-squirixs-wal-in-preview6-5ha4"&gt;preview.6 article&lt;/a&gt;, I wrote about what happens after a write is accepted: binary journal frames, group commit, crash recovery, and durable retries. That work made the durability boundaries easier to name.&lt;/p&gt;

&lt;p&gt;It does not answer the next uncomfortable question: what happens when the node should &lt;strong&gt;stop accepting&lt;/strong&gt; more durable work?&lt;/p&gt;

&lt;p&gt;A journal that never refuses appends will eventually fill the disk. A shared concurrency gate that never distinguishes callers will let one noisy tenant starve everyone else. From the outside, both failures can look like "the cache is down," even while the process is still alive.&lt;/p&gt;

&lt;p&gt;Those questions shaped &lt;code&gt;0.1.0-preview.7&lt;/code&gt;: journal disk quotas that reject durable writes with a stable error, and per-principal backpressure keyed by JWT subject (or connection id when there is no subject).&lt;/p&gt;

&lt;p&gt;Squirix is still an experimental preview, not a production-ready cache. Its APIs and storage formats may change during &lt;code&gt;0.x&lt;/code&gt;. This article describes the current design and the reasoning behind it, not a compatibility promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode we wanted to avoid
&lt;/h2&gt;

&lt;p&gt;Imagine a node that keeps appending journal segments until the volume is full. Depending on the OS and I/O path, the next write might throw, hang, or leave a torn tail. Operators then see process crashes, opaque I/O exceptions, and readiness flaps that take the node out of load balancers — exactly when health and metrics are most useful.&lt;/p&gt;

&lt;p&gt;The contract we want instead is boring and explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;journal on-disk size approaches configured cap
                |
                v
durable append that would exceed the cap is rejected
                |
                v
client gets JOURNAL_DISK_QUOTA
(gRPC ResourceExhausted)
                |
                v
/health/ready stays healthy
/health/ready/details exposes journalDisk pressure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rejection is definitive for that attempt. The process stays up. Readiness stays scrapeable so operators can reclaim space or raise the limit without first fighting a dead host.&lt;/p&gt;

&lt;p&gt;That is different from the "commit unknown" case in preview.6. There, durable bytes may already exist and the client only saw a timeout. Here, the server refused the append before it crossed the durability boundary. Clients should not invent success; they should back off until operators reclaim headroom or raise the cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hard cap with soft observability
&lt;/h2&gt;

&lt;p&gt;Pipelined journal settings already expose &lt;code&gt;JournalMaxTotalBytesMb&lt;/code&gt; — a hard on-disk size for &lt;strong&gt;journal segments&lt;/strong&gt;, not snapshots or manifests. Preview.7 maps that cap into controlled write rejection. Segment-count and per-segment size caps can also reject with the same &lt;code&gt;JOURNAL_DISK_QUOTA&lt;/code&gt; code.&lt;/p&gt;

&lt;p&gt;The soft high-water for readiness details is fixed at 80% of the hard limit. Soft high-water is observability only: writes that still fit under the hard cap continue. Hard limit (&lt;code&gt;critical&lt;/code&gt;) is the observed state when &lt;code&gt;usedBytes &amp;gt;= maxBytes&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0% -------- 80% (high) -------- 100% (critical / hard cap)
|              |                      |
|   writes OK  |  writes OK if fit;   |  writeRejectionActive=true
|              |  oversize append     |  (usage already at cap)
|              |  still rejected      |
|              |  with JOURNAL_DISK_  |
|              |  QUOTA               |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important timing detail: rejection uses &lt;code&gt;usedBytes + appendBytes &amp;gt; maxBytes&lt;/code&gt;. An append can therefore be rejected while &lt;code&gt;state&lt;/code&gt; is still &lt;code&gt;high&lt;/code&gt; and &lt;code&gt;writeRejectionActive&lt;/code&gt; is still &lt;code&gt;false&lt;/code&gt;. &lt;code&gt;writeRejectionActive&lt;/code&gt; becomes &lt;code&gt;true&lt;/code&gt; only once usage has already reached the hard cap.&lt;/p&gt;

&lt;p&gt;A few details matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The quota covers journal segments only.&lt;/li&gt;
&lt;li&gt;Rejected durable writes do not crash the process.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health/ready&lt;/code&gt; remains healthy so the node can still expose pressure state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health/ready/details&lt;/code&gt; includes &lt;code&gt;journalDisk&lt;/code&gt; with &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;maxBytes&lt;/code&gt;, &lt;code&gt;usedBytes&lt;/code&gt;, &lt;code&gt;highWaterBytes&lt;/code&gt;, and &lt;code&gt;writeRejectionActive&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operator path is intentionally unexciting: confirm pressure, trigger or wait for snapshot plus compaction/retention, raise &lt;code&gt;JournalMaxTotalBytesMb&lt;/code&gt; only after confirming disk capacity, and treat &lt;code&gt;JOURNAL_DISK_QUOTA&lt;/code&gt; like other capacity rejections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why readiness stays healthy under quota
&lt;/h2&gt;

&lt;p&gt;It is tempting to mark a node not-ready when the journal is full. That can remove it from traffic, but it also removes the cheapest way to inspect &lt;em&gt;why&lt;/em&gt; durable writes stopped.&lt;/p&gt;

&lt;p&gt;Preview.7 keeps &lt;code&gt;/health/ready&lt;/code&gt; healthy and puts pressure into details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load balancer / orchestrator
        |
        | GET /health/ready           --&amp;gt; 200 (still ready)
        |
        | GET /health/ready/details
        v
  journalDisk.state = critical
  writeRejectionActive = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache clients already receive gRPC &lt;code&gt;ResourceExhausted&lt;/code&gt; with public code &lt;code&gt;JOURNAL_DISK_QUOTA&lt;/code&gt;. That is the write-path signal. Readiness is the operator-path signal. Mixing them turns every capacity event into a membership event.&lt;/p&gt;

&lt;p&gt;HTTP on the primary listener is limited to health and metrics in v0.1; there is no public REST cache mutation surface. Shared error helpers can project the same logical error as HTTP 429 in tests and internal adapters, but application clients should key off the gRPC status and &lt;code&gt;JOURNAL_DISK_QUOTA&lt;/code&gt; code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backpressure is not memory pressure
&lt;/h2&gt;

&lt;p&gt;Squirix already separates several admission policies. Preview.7 does not merge them; it makes one of them fairer under multi-tenant load.&lt;/p&gt;

&lt;p&gt;Transport limits still protect auth, payload size, and deadlines. Memory pressure still admits work based on estimated cache size. Journal disk quota still protects on-disk journal growth. Runtime backpressure protects concurrent cache operations, queues, slowdown, and optional rate limits.&lt;/p&gt;

&lt;p&gt;Runtime backpressure sits after validation and before memory admission. A rejection there happens before logical cache operations enter memory admission or clustered/local paths, so rejected work does not append journal records, mutate local memory, update memory accounting, or record idempotency outcomes.&lt;/p&gt;

&lt;p&gt;That is the same "fail closed before durability" idea as memory admission — applied to concurrency and rate, not to estimated bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolating callers by principal
&lt;/h2&gt;

&lt;p&gt;Global in-flight and queue limits protect the node. They do not stop one authenticated client from consuming the whole budget.&lt;/p&gt;

&lt;p&gt;Preview.7 resolves a &lt;strong&gt;backpressure client id&lt;/strong&gt; for each cache operation and applies per-client concurrency and rate limits against that id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request
  |
  +--&amp;gt; HttpContext missing? ----------- yes --&amp;gt; runtime
  |
  +--&amp;gt; authenticated subject? --------- yes --&amp;gt; jwt:{subject}
  |
  +--&amp;gt; connection id present? --------- yes --&amp;gt; conn:{connectionId}
  |
  +-------------------------------------------&amp;gt; runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Authenticated external callers with a subject land in &lt;code&gt;jwt:{subject}&lt;/code&gt; (&lt;code&gt;NameIdentifier&lt;/code&gt;, then inbound &lt;code&gt;sub&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Anonymous or subject-less HTTP callers land in &lt;code&gt;conn:{connectionId}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In-process callers without an &lt;code&gt;HttpContext&lt;/code&gt; share one &lt;code&gt;runtime&lt;/code&gt; bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v0.1 external auth is JWT-only. Inter-node cluster forwarding uses mTLS on the internal listener and typically lands in the &lt;code&gt;conn:&lt;/code&gt; or &lt;code&gt;runtime&lt;/code&gt; bucket rather than a shared external JWT subject. That is deliberate: external tenant isolation should not treat owner-forwarded RPCs as one giant client.&lt;/p&gt;

&lt;p&gt;Anonymous loopback clients on distinct TCP connections also do not share one JWT principal bucket. Without a principal, connection identity is the best stable key the host has.&lt;/p&gt;

&lt;h2&gt;
  
  
  What preview.7 changes
&lt;/h2&gt;

&lt;p&gt;The pressure work in &lt;code&gt;0.1.0-preview.7&lt;/code&gt; includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mapping the journal total-size hard cap to controlled durable write rejection (&lt;code&gt;JOURNAL_DISK_QUOTA&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;exposing soft high-water (80%) on readiness details without blocking writes that still fit;&lt;/li&gt;
&lt;li&gt;keeping readiness healthy under quota so pressure remains observable;&lt;/li&gt;
&lt;li&gt;keying per-client backpressure off JWT subject, then connection id, then the shared &lt;code&gt;runtime&lt;/code&gt; bucket;&lt;/li&gt;
&lt;li&gt;tightening path validation and symlink helpers, and clearing analyzer/Sonar debt on client and server hot paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of that is housekeeping. Most of it exists because awkward timing is not only about crashes. A full volume, a noisy neighbor, or a readiness flap at the wrong moment can erase the operational story as thoroughly as a torn journal frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  What preview.7 does not promise
&lt;/h2&gt;

&lt;p&gt;This work makes overload and disk exhaustion easier to reason about, but Squirix is still early software.&lt;/p&gt;

&lt;p&gt;Quotas are per node and cover journal segments on that node; there is no cluster-wide disk accountant. In-process callers share &lt;code&gt;runtime&lt;/code&gt;, and internal mTLS paths are not the same as external JWT tenants. Quota buys a clear error and time to act; it is not a substitute for capacity planning, compaction, backups, or recovery testing.&lt;/p&gt;

&lt;p&gt;Preview.7 is stronger operator-facing pressure control on top of the preview.6 durability foundation, not a production-readiness declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Durability work asks: what survives a crash? Pressure work asks: what happens when survival is no longer free?&lt;/p&gt;

&lt;p&gt;A journal that always accepts appends will eventually lose the ability to flush cleanly. A concurrency gate that treats every caller as interchangeable will eventually amplify a noisy neighbor into a whole-node outage.&lt;/p&gt;

&lt;p&gt;The difficult part is naming the refusal precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rejected before durability (journal quota and backpressure both fail closed early);&lt;/li&gt;
&lt;li&gt;rejected for this principal only when the signal is backpressure — journal disk quota remains node-wide;&lt;/li&gt;
&lt;li&gt;still ready enough to inspect;&lt;/li&gt;
&lt;li&gt;safe for clients to treat as definitive for that attempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Preview.7 makes those refusals more explicit. That gives operators a capacity policy instead of unexplained I/O chaos, and it gives future multi-tenant work a clearer place to hang fairness without rewriting the journal story.&lt;/p&gt;

&lt;p&gt;If you operate multi-tenant .NET services or care about cache overload semantics, I would be glad to hear where this model still surprises you. Feedback on quota versus readiness, JWT versus connection isolation, and client reactions to &lt;code&gt;JOURNAL_DISK_QUOTA&lt;/code&gt; is especially welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/squirix/squirix" rel="noopener noreferrer"&gt;Squirix on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/squirix/squirix/releases/tag/v0.1.0-preview.7" rel="noopener noreferrer"&gt;Release &lt;code&gt;v0.1.0-preview.7&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/squirix/squirix/blob/main/docs/release-notes/v0.1.0.md" rel="noopener noreferrer"&gt;Squirix 0.1.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/__2d3e61e/what-happens-after-a-write-reworking-squirixs-wal-in-preview6-5ha4"&gt;What happens after a write? (preview.6)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/squirix/squirix/blob/main/docs/operational-runbook.md#journal-disk-quota" rel="noopener noreferrer"&gt;Journal disk quota runbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/squirix/squirix/blob/main/docs/configuration.md#backpressure" rel="noopener noreferrer"&gt;Backpressure configuration&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package squirix &lt;span class="nt"&gt;--version&lt;/span&gt; 0.1.0-preview.7
dotnet add package squirix.server &lt;span class="nt"&gt;--version&lt;/span&gt; 0.1.0-preview.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>dotnet</category>
      <category>distributedsystems</category>
      <category>csharp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What happens after a write? Reworking Squirix's WAL in preview.6</title>
      <dc:creator>Alex E</dc:creator>
      <pubDate>Sun, 19 Jul 2026 07:51:43 +0000</pubDate>
      <link>https://dev.to/__2d3e61e/what-happens-after-a-write-reworking-squirixs-wal-in-preview6-5ha4</link>
      <guid>https://dev.to/__2d3e61e/what-happens-after-a-write-reworking-squirixs-wal-in-preview6-5ha4</guid>
      <description>&lt;p&gt;In my&lt;br&gt;
&lt;a href="https://dev.to/__2d3e61e/why-squirix-uses-a-strict-clientserver-architecture-for-a-net-distributed-cache-5086"&gt;first Squirix article&lt;/a&gt;,&lt;br&gt;
I wrote about why Squirix keeps a strict boundary between the client and the server. Applications use a typed client&lt;br&gt;
over gRPC; the server owns cache state, routing, persistence, recovery, and operational endpoints.&lt;/p&gt;

&lt;p&gt;That boundary makes ownership clear. It does not answer the uncomfortable question underneath it: what exactly happens&lt;br&gt;
between accepting a write and telling the client it succeeded?&lt;/p&gt;

&lt;p&gt;Preview.6 goes one level deeper into that path. What if the process dies after writing to disk but before sending the&lt;br&gt;
response? What survives a torn journal tail? How can a client retry without applying the same mutation twice?&lt;/p&gt;

&lt;p&gt;Those questions shaped the release: a pipelined binary write-ahead log, tighter snapshot and journal recovery, and&lt;br&gt;
idempotent mutation outcomes that survive a restart.&lt;/p&gt;

&lt;p&gt;Squirix is still an experimental preview, not a production-ready cache. Its APIs and storage formats may change during&lt;br&gt;
&lt;code&gt;0.x&lt;/code&gt;. This article describes the current design and the reasoning behind it, not a compatibility promise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Follow one write
&lt;/h2&gt;

&lt;p&gt;The basic ordering rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Append the mutation to the journal before applying it in memory, and do not return success before the required&lt;br&gt;
durability boundary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a normal durable mutation, the path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate and admit the mutation
            |
            v
append a binary WAL record
            |
            v
cross the configured durability boundary
            |
            v
apply the mutation in memory
            |
            v
return success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is deliberate. If the server changed memory first and appended to the journal afterward, a crash in between&lt;br&gt;
could make an acknowledged write disappear after restart.&lt;/p&gt;

&lt;p&gt;Idempotent RPCs take one deliberate variation. Squirix appends the mutation, applies it in memory to produce the&lt;br&gt;
response, appends that response as an idempotency outcome, then waits for a single durability boundary covering both&lt;br&gt;
records. The response is not returned before that wait completes.&lt;/p&gt;

&lt;p&gt;The mutation and its retry result share one durability commit, without pretending the network response is atomic with&lt;br&gt;
the disk write.&lt;/p&gt;

&lt;p&gt;Journal-first ordering avoids acknowledged-but-unrecoverable writes, but it exposes a different case. Suppose the&lt;br&gt;
journal becomes durable and the process dies before the response reaches the client. Recovery may replay the mutation&lt;br&gt;
even though the client observed only a timeout or broken connection.&lt;/p&gt;

&lt;p&gt;That outcome is not an ordinary failure. It is &lt;strong&gt;commit unknown&lt;/strong&gt;: the operation may have committed, but the client did&lt;br&gt;
not receive a definitive answer.&lt;/p&gt;

&lt;p&gt;This is different from a rejection before the journal. If the memory-admission gate rejects a growing entry, Squirix&lt;br&gt;
has not appended anything and has not changed cache state. That rejection is definitive. Once durable bytes may exist,&lt;br&gt;
the answer has to be more careful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Moving file I/O off request threads
&lt;/h2&gt;

&lt;p&gt;Preview.6 replaces the previous journal path with a pipelined binary WAL backend. The goal was not simply to make file&lt;br&gt;
writes faster. It was to give journal work one owner and make backpressure visible.&lt;/p&gt;

&lt;p&gt;Request threads do not independently open files, encode records, and call &lt;code&gt;fsync&lt;/code&gt;. They submit append work to a bounded&lt;br&gt;
ring. A dedicated background thread named &lt;code&gt;squirix-journal-io&lt;/code&gt; owns the journal event loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request threads
   |    |    |
   v    v    v
+-------------------+
| bounded WAL ring  |
+-------------------+
          |
          v
+------------------------+
| single journal thread  |
+------------------------+
          |
          v
   binary WAL segments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The journal thread drains accepted work, encodes and batches frames, writes segment bytes, services durability&lt;br&gt;
deadlines, performs flushes, and coordinates segment rolls.&lt;/p&gt;

&lt;p&gt;The queue is bounded because storage can always fall behind incoming traffic. An unbounded queue would turn that&lt;br&gt;
mismatch into unbounded memory growth; the ring turns it into visible backpressure instead.&lt;/p&gt;

&lt;p&gt;The single writer also gives segment state one clear owner. Offsets, batches, durability state, and roll decisions do&lt;br&gt;
not need to be coordinated among arbitrary request threads.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the journal is binary
&lt;/h2&gt;

&lt;p&gt;The new WAL writes binary frames rather than passing persistence records through JSON text.&lt;/p&gt;

&lt;p&gt;That gives the storage path predictable record sizes, explicit field validation, CRC32C checksums, and span-based&lt;br&gt;
encoding for the hot parts of the format. It also makes versioning a property of the storage format rather than an&lt;br&gt;
accidental consequence of a general-purpose serializer.&lt;/p&gt;

&lt;p&gt;This is partly a performance choice, but the main benefit is control. Recovery needs to distinguish a complete record&lt;br&gt;
from arbitrary bytes at the end of a file. A framed binary format can say exactly how long a record is and whether its&lt;br&gt;
checksum is valid.&lt;/p&gt;

&lt;p&gt;The event loop also coalesces encoded records into write batches. Writing bytes and declaring those bytes durable are&lt;br&gt;
separate steps, which is where group commit enters the design.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sharing an &lt;code&gt;fsync&lt;/code&gt; with group commit
&lt;/h2&gt;

&lt;p&gt;Flushing every mutation separately gives straightforward semantics, but it also makes each request pay the full&lt;br&gt;
durability cost.&lt;/p&gt;

&lt;p&gt;With group commit enabled, several appends can share one flush:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;append A ─┐
append B ─┼── write batch ── fsync ── complete A, B, and C
append C ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each waiter is completed only after a durability flush covers the relevant appends. The journal thread flushes the&lt;br&gt;
current write batch, performs the storage flush, and then completes the captured batch of waiters. If the flush fails,&lt;br&gt;
every waiter in that batch fails; no caller is told that partial durability was enough.&lt;/p&gt;

&lt;p&gt;The group is bounded by configuration—a maximum wait and a maximum batch size—trading a small, controlled delay for&lt;br&gt;
fewer durability flushes. Workloads that prefer the strictest per-write latency can disable group commit.&lt;/p&gt;

&lt;p&gt;The important contract is not merely that an &lt;code&gt;fsync&lt;/code&gt; happened somewhere. It is that a completed waiter is covered by&lt;br&gt;
that durability flush.&lt;/p&gt;
&lt;h2&gt;
  
  
  The response can still get lost
&lt;/h2&gt;

&lt;p&gt;Even with correct WAL ordering, the server cannot make the network response atomic with the disk write.&lt;/p&gt;

&lt;p&gt;The failure window is easiest to see as a short sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. append operation A
2. flush operation A
3. crash before the response reaches the client
4. recover operation A from the WAL
5. client retries operation A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a stable request identity, step 5 could apply the same logical mutation twice. The risk is greatest for&lt;br&gt;
mutations whose effect is not naturally idempotent.&lt;/p&gt;

&lt;p&gt;Squirix mutation RPCs therefore carry an opaque &lt;code&gt;operation_id&lt;/code&gt;. The server associates the ID with a request fingerprint&lt;br&gt;
and the serialized response. A retry with the same ID and fingerprint can return the original result. Reusing the ID&lt;br&gt;
for a different request is rejected.&lt;/p&gt;

&lt;p&gt;In preview.6, that identity survives restart. Recovery rebuilds successful durable outcomes from operation metadata in&lt;br&gt;
the journal and from retained idempotency records in snapshots. The operation ID is also preserved when a receiving&lt;br&gt;
node forwards a mutation to the key owner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client
  |
  | operation_id = 4f...
  v
receiving node
  |
  | same operation_id
  v
owner node
  |
  +--&amp;gt; durable mutation record
  |
  +--&amp;gt; durable outcome metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a distributed transaction, and I will not present it as one. It gives a supported mutation a stable identity&lt;br&gt;
across forwarding, timeouts, retries, and restarts—exactly the situations where commit unknown becomes operationally&lt;br&gt;
important.&lt;/p&gt;
&lt;h2&gt;
  
  
  Recovery starts by distrusting the files
&lt;/h2&gt;

&lt;p&gt;A snapshot is not valid merely because a file with the right name exists. Squirix reads and validates the complete&lt;br&gt;
snapshot into a temporary load result before applying its entries. The startup gate remains closed until snapshot&lt;br&gt;
restore and journal replay have both completed.&lt;/p&gt;

&lt;p&gt;The current recovery flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the manifest.&lt;/li&gt;
&lt;li&gt;Locate the referenced snapshot.&lt;/li&gt;
&lt;li&gt;Decode cache entries and retained idempotency records into a temporary load result.&lt;/li&gt;
&lt;li&gt;Validate the complete snapshot.&lt;/li&gt;
&lt;li&gt;Apply the validated snapshot state.&lt;/li&gt;
&lt;li&gt;Replay journal records after the snapshot watermark.&lt;/li&gt;
&lt;li&gt;Open the startup gate.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manifest
   |
   v
snapshot ── validate all frames ── apply recovered state
   |
   v
last applied sequence N
   |
   v
replay WAL records where sequence &amp;gt; N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The watermark matters. A snapshot represents state through a particular journal sequence. Replaying records at or&lt;br&gt;
before that sequence would apply mutations twice; skipping later records would lose committed state.&lt;/p&gt;

&lt;p&gt;Preview.6 freezes the metadata that describes the snapshot cut so that the entries and their sequence boundary refer&lt;br&gt;
to the same logical point.&lt;/p&gt;

&lt;p&gt;Snapshot publication follows the usual temporary-file pattern: write and flush a temporary file, publish the completed&lt;br&gt;
snapshot, and only then update the manifest reference. A crash during the temporary write must not make an incomplete&lt;br&gt;
snapshot authoritative.&lt;/p&gt;

&lt;p&gt;If the referenced snapshot is missing, unreadable, truncated, or fails checksum validation, Squirix discards all&lt;br&gt;
snapshot-derived state. It falls back to journal-only recovery only when the required journal history is still&lt;br&gt;
available. Otherwise recovery fails instead of guessing across a gap. Preview.6 also fixes the valid fallback path so&lt;br&gt;
replay starts from the earliest available segment when there is no snapshot watermark.&lt;/p&gt;
&lt;h2&gt;
  
  
  A torn tail does not erase the valid prefix
&lt;/h2&gt;

&lt;p&gt;A process can stop halfway through the final WAL frame. Recovery must not interpret those bytes as a complete mutation,&lt;br&gt;
but it should not discard every earlier record either.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[valid frame][valid frame][valid frame][partial bytes...]
                                      ^
                               recovery stops here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Journal replay proceeds through complete, validated records in segment order. During startup, Squirix scans the active&lt;br&gt;
segment to the last valid frame boundary and truncates an incomplete tail before reopening it for writes.&lt;/p&gt;

&lt;p&gt;This is intentionally conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never invent a record from partial bytes;&lt;/li&gt;
&lt;li&gt;never replay beyond an invalid boundary;&lt;/li&gt;
&lt;li&gt;preserve the valid committed prefix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same principle applies to snapshots: do not start applying entries until the complete file has been decoded and&lt;br&gt;
validated. A clean journal fallback is easier to reason about than trusting a partly decoded snapshot.&lt;/p&gt;
&lt;h2&gt;
  
  
  Compaction must preserve retry semantics
&lt;/h2&gt;

&lt;p&gt;Compaction cannot keep only the latest key/value state.&lt;/p&gt;

&lt;p&gt;Imagine that operation A committed, its result was retained for deduplication, and old journal segments were then&lt;br&gt;
compacted away. If the snapshot contained cache values but not the idempotency record, a delayed retry of A could lose&lt;br&gt;
its original identity and execute again.&lt;/p&gt;

&lt;p&gt;Snapshots therefore include retained idempotency outcomes alongside cache entries. After compaction and restart,&lt;br&gt;
Squirix needs to recover both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cache state
+
retry identity and durable outcomes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Durability is not only the ability to reconstruct values. It also means reconstructing enough protocol state to handle&lt;br&gt;
uncertain retries safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What preview.6 changes
&lt;/h2&gt;

&lt;p&gt;The durability work in &lt;code&gt;0.1.0-preview.6&lt;/code&gt; includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a pipelined binary WAL;&lt;/li&gt;
&lt;li&gt;a dedicated single-writer journal event loop;&lt;/li&gt;
&lt;li&gt;bounded append backpressure;&lt;/li&gt;
&lt;li&gt;configurable group commit;&lt;/li&gt;
&lt;li&gt;journal-only recovery from the earliest available segment when the journal topology is valid;&lt;/li&gt;
&lt;li&gt;a stable snapshot cut and replay watermark;&lt;/li&gt;
&lt;li&gt;durable operation outcomes across restart;&lt;/li&gt;
&lt;li&gt;operation-ID propagation between nodes;&lt;/li&gt;
&lt;li&gt;retained idempotency records in snapshots;&lt;/li&gt;
&lt;li&gt;compaction cancellation during shutdown;&lt;/li&gt;
&lt;li&gt;lower allocation pressure on WAL hot paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some changes improve throughput. Most exist because of awkward timing: a crash after a journal flush, a lost response,&lt;br&gt;
a truncated final frame, compaction followed by an old retry, or shutdown while persistence work is active.&lt;/p&gt;

&lt;p&gt;That is where durability engineering lives. The happy path is necessary, but it is rarely the part that keeps me&lt;br&gt;
thinking after the code is written.&lt;/p&gt;

&lt;h2&gt;
  
  
  What preview.6 does not promise
&lt;/h2&gt;

&lt;p&gt;This work makes the durability model easier to reason about, but Squirix is still early software.&lt;/p&gt;

&lt;p&gt;The on-disk format may change during &lt;code&gt;0.x&lt;/code&gt;. Durability is per node; preview.6 does not add replication or automatic&lt;br&gt;
failover. Operations spanning multiple owners are not globally atomic transactions. Serializer compatibility and&lt;br&gt;
restart behavior still need to be validated for the mutation paths a workload depends on.&lt;/p&gt;

&lt;p&gt;A WAL is not a substitute for replication, backups, or recovery testing. Preview.6 is a stronger durability foundation,&lt;br&gt;
not a production-readiness declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Appending bytes is the easy part of a write-ahead log. Deciding what those bytes mean after a crash is the real work.&lt;/p&gt;

&lt;p&gt;The difficult part is naming every boundary precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepted;&lt;/li&gt;
&lt;li&gt;appended;&lt;/li&gt;
&lt;li&gt;flushed;&lt;/li&gt;
&lt;li&gt;applied in memory;&lt;/li&gt;
&lt;li&gt;visible to the client;&lt;/li&gt;
&lt;li&gt;recoverable after restart;&lt;/li&gt;
&lt;li&gt;safe to retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Preview.6 makes those boundaries more explicit. That gives future work—replication, failover, and more advanced cluster&lt;br&gt;
behavior—a durability model it can build on instead of work around.&lt;/p&gt;

&lt;p&gt;If you work on storage engines or distributed .NET systems, I would be glad to hear where you think the remaining sharp&lt;br&gt;
edges are. The project is open source, and feedback on the WAL, recovery semantics, and retry model is especially&lt;br&gt;
welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/squirix/squirix" rel="noopener noreferrer"&gt;Squirix on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/squirix/squirix/blob/main/docs/release-notes/v0.1.0.md" rel="noopener noreferrer"&gt;Squirix 0.1.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>distributedsystems</category>
      <category>squirix</category>
    </item>
    <item>
      <title>Why Squirix uses a strict client/server architecture for a .NET distributed cache</title>
      <dc:creator>Alex E</dc:creator>
      <pubDate>Sun, 07 Jun 2026 07:12:52 +0000</pubDate>
      <link>https://dev.to/__2d3e61e/why-squirix-uses-a-strict-clientserver-architecture-for-a-net-distributed-cache-5086</link>
      <guid>https://dev.to/__2d3e61e/why-squirix-uses-a-strict-clientserver-architecture-for-a-net-distributed-cache-5086</guid>
      <description>&lt;p&gt;Squirix 0.1.0 is an early preview of a .NET distributed cache. A typed client SDK talks to a remote server over gRPC; the server owns state, routing, durability, and operational endpoints.&lt;/p&gt;

&lt;p&gt;This is the direction I am validating in 0.1.0 — not a claim that every cache must work this way. Embedded designs are fine for many workloads. Squirix targets a different shape: &lt;strong&gt;the application stays a client; the server owns the data lifecycle.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just a cache library"
&lt;/h2&gt;

&lt;p&gt;A cache library is simple until you ask who owns what. When cache logic runs inside your app process, state, memory pressure, and persistence share the app's lifecycle. That works for local acceleration (&lt;code&gt;IMemoryCache&lt;/code&gt;), but gets ambiguous when you need shared state across instances, durability across restarts, independent health/metrics, or cluster routing.&lt;/p&gt;

&lt;p&gt;At that point you often have an implicit server with unclear boundaries. Squirix makes the split explicit from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded mode vs client/server mode
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Embedded&lt;/strong&gt; — cache logic in or tightly coupled to the app process: in-memory caches, libraries that hide remote I/O, or a co-located server called via direct references. Low friction; you rarely expect separate health probes or journal compaction on "just a dependency."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client/server&lt;/strong&gt; — the app holds no authoritative state. It connects over a wire contract; the server owns placement, mutations, durability, recovery, and admin/metrics endpoints. Redis and most production distributed caches follow this shape.&lt;/p&gt;

&lt;p&gt;Squirix 0.1.0 is client/server first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;application -&amp;gt; Squirix client SDK -&amp;gt; Squirix.Server node(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two packages, enforced at build time: &lt;a href="https://www.nuget.org/packages/squirix/" rel="noopener noreferrer"&gt;&lt;code&gt;squirix&lt;/code&gt;&lt;/a&gt; (client) and &lt;code&gt;squirix.server&lt;/code&gt; (runtime). The server does not reference the client assembly.&lt;/p&gt;

&lt;p&gt;You can embed the server in ASP.NET Core via &lt;code&gt;AddSquirixServer&lt;/code&gt; / &lt;code&gt;MapSquirixServer&lt;/code&gt;, but application access still goes through &lt;code&gt;SquirixClient.ConnectAsync(...)&lt;/code&gt; — even in the same process. That is hosting convenience, not embedded cache semantics in the app layer.&lt;/p&gt;

&lt;p&gt;Embedded mode is not bad — it optimizes for different goals. Squirix targets shared remote state, server-owned durability, and operability as infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Squirix chooses client/server first
&lt;/h2&gt;

&lt;p&gt;Reasoning behind the 0.1.0 shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operational boundary&lt;/strong&gt; — deploy, probe, and upgrade cache nodes independently. Health (&lt;code&gt;/health/live&lt;/code&gt;, &lt;code&gt;/health/ready&lt;/code&gt;), admin (&lt;code&gt;/admin/whoami&lt;/code&gt;, &lt;code&gt;/admin/ring&lt;/code&gt;), and Prometheus at &lt;code&gt;/metrics&lt;/code&gt; live on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-owned lifecycle&lt;/strong&gt; — WAL journal, snapshots, compaction, and recovery stay in &lt;code&gt;squirix.server&lt;/code&gt;, not in every application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighter client package&lt;/strong&gt; — run the server as its own process or container; apps only reference the client SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustering path&lt;/strong&gt; — static consistent-hash routing in 0.1.0 is early, but routing and failover can evolve server-side without rewriting &lt;code&gt;ICache&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability isolation&lt;/strong&gt; — recovery and compaction failures stay out of application request threads while semantics harden.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this affects the public API
&lt;/h2&gt;

&lt;p&gt;You connect via &lt;code&gt;SquirixClient.ConnectAsync(...)&lt;/code&gt; and work with typed &lt;code&gt;ICache&amp;lt;T&amp;gt;&lt;/code&gt; and explicit read results (&lt;code&gt;CacheValueResult&amp;lt;T&amp;gt;&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Squirix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SquirixClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"http://localhost:5001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetCacheAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lookup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production server hosting uses &lt;code&gt;AddSquirixServer&lt;/code&gt; / &lt;code&gt;MapSquirixServer&lt;/code&gt;. Transport is &lt;strong&gt;gRPC&lt;/strong&gt; (&lt;code&gt;SquirixCache.proto&lt;/code&gt;); cache operations, health, and admin routes are also available over &lt;strong&gt;HTTP/2 REST&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists in Squirix 0.1.0
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;0.1.0-preview.1&lt;/code&gt;&lt;/strong&gt;, .NET 10 only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client/server split (&lt;code&gt;squirix&lt;/code&gt; + &lt;code&gt;squirix.server&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Typed &lt;code&gt;ICache&amp;lt;T&amp;gt;&lt;/code&gt; — basic KV + expiration&lt;/li&gt;
&lt;li&gt;gRPC + HTTP/2 REST cache endpoints&lt;/li&gt;
&lt;li&gt;Per-node journal, snapshots, compaction, recovery&lt;/li&gt;
&lt;li&gt;Health, readiness, admin routes; Prometheus metrics; OpenTelemetry journal tracing&lt;/li&gt;
&lt;li&gt;Static consistent-hash single-owner routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local h2c dev: &lt;code&gt;$env:DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT = "1"&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still experimental
&lt;/h2&gt;

&lt;p&gt;Early preview — &lt;strong&gt;not production-ready&lt;/strong&gt;. API, wire format, and on-disk layouts may change during 0.x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/squirix/squirix" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages/squirix/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; · &lt;a href="https://github.com/squirix/squirix/blob/main/docs/release-notes/v0.1.0.md" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/squirix/squirix/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/squirix/squirix/blob/main/docs/server-mode.md" rel="noopener noreferrer"&gt;Server mode&lt;/a&gt; · &lt;a href="https://github.com/squirix/squirix/blob/main/docs/diagnostics.md" rel="noopener noreferrer"&gt;Diagnostics&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>distributedsystems</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
