<?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>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>
