Repository navigation
Releases
WasmKeep v0.3.0
Reliable enough to leave running. This release fixes the known ways
a cluster stalled or lost an answer when a node or the bucket misbehaved,
keeps a request for a dead node's actor until the takeover instead of
refusing it, and adds the guides that running an application on a cluster
showed missing. Still a pre-alpha pre-release: for trying WasmKeep,
side projects and staging, not production. The interface between an actor
and the host is unchanged (wasmkeep:actor@0.2.0), and so are the SDKs'
APIs: a component built for 0.1.0 or 0.2.0 runs as it is. See the
roadmap for what comes next.
Soaked on three nodes against a real bucket, a node killed every three
minutes: twelve hours on a release candidate (239 kills, failover p99
22.43 s, no acknowledged write lost or duplicated, no alarm missed;
bench/results/2026-11-12-b849bfe-soak-ovh-bhs-3vps-12h.csv), then three
hours on this release's code (59 kills, failover p99 20.9 s, the same
result; bench/results/2026-11-13-648c2d1-soak-ovh-bhs-3vps-3h.csv).
What changed
- A request waits for a takeover. When an actor's node dies, a GET or
HEAD without a body, or a WebSocket upgrade, for one of its actors is
kept by the node that received it until the actor can be taken over (15
to about 21 s after the dead node's last write, unchanged), then served,
instead of being answered 503 meanwhile. A request with a body, an
actor-to-actor call and a forwarded request still get the 503 at once,
which still means it did not run. A client that leaves while its
request is kept is dropped, and its request never runs. At most a
quarter of the node's connection limit, and 64 per actor, are kept;
beyond that, the 503 at once.--failover-wait-secssets the wait
(about 25 s by default, from the lease timing); 0 turns it off. Set the
load balancer's server timeout above it.
wasmkeep_failover_waits_totalcounts how each wait ended
(what a caller sees,
front door). - A snapshot upload is bounded, tried again after a failure, and
counted. The snapshot an actor's owner adds every 64 writes holds the
actor while it uploads. The upload had no bound of its own, so a bucket
that left it unanswered held the actor until the bucket client ended the
request (30 s by default). It now gets one attempt, given up after 5 s
plus a second per megabyte, 10 s at most. A snapshot that fails costs no
write and fails no event, and is tried again once the actor has made one
more write and 30 s have passed, then 8 writes later, then every 64; a
failure used to put the next snapshot 64 more writes away. The node
keeps these waits while an idle actor is out of its memory.
wasmkeep_snapshot_upload_failures_totalcounts the snapshot uploads
that were not acknowledged
(replication,
monitoring). - A restore's reads have one deadline, a refused read is sent again,
and failed restores are counted. Restoring an actor from the bucket
bounded each read but not the reads together, so reads that each stayed
under their limit added up with the length of the history while every
caller of the actor waited. The reads of one restore now end within
30 s, plus a second per megabyte of what it reads and a second per
eight objects. The clock runs only while the restore waits for the
bucket to give it the object it applies next: the listing, work on the
local disk and the wait for a turn among the node's other restores are
not counted. Neither are the seconds that other restores' reads on the
node add to the limit of a read (a second per megabyte in flight, 256 at
most), so restores that read side by side after a takeover are not
ended for each other's bytes; a restore alone on the node has no more
than its own time. Running out of time is answered like a bucket that
does not answer, a 503 withRetry-After, and leaves the local file as
it was. A read the bucket refuses with an error a resend can get past
(a 5xx or a broken connection, after the bucket client's own retries)
used to fail the whole restore; it is now sent again after 0.5 s, then
1 s, three tries per object.wasmkeep_restore_failures_totalcounts
the restores that did not complete
(replication,
monitoring). - A request body the handler left unread no longer costs the upload or
the answer. A handler may answer without reading the body. The server
now reads and discards what is left, up to the body cap and for at most
30 s, so a large upload is no longer cut off and, when the request came
through another node, its answer is no longer lost. A body held back
withExpect: 100-continueis not asked for, and a node that forwards a
request does not passExpecton (concepts). - Between nodes, a WebSocket upgrade the owner never read is sent once
more, and a forwarded request that was written is never sent twice.
An upgrade relayed to the actor's node that the node provably never read
(it refused the TLS handshake, or the upgrade came back unsent on a new
TLS connection) is sent once more while the client waits; it used to
get a 503 asking the client to retry. Between nodes, the only other
thing sent a second time is anactors.callthe peer provably never
read in the same way, once, as 0.2.0 already did after a refused
handshake. A forwarded request is never sent again, whether its
connection could not be opened or it was written into a connection that
then failed: one that was written may have run on the owner, so the
client gets a 503, which does not say whether it ran
(running a cluster). - A client that only listens no longer misses a room's messages
without a close. A room that hibernates keeps its WebSockets open on
its node and releases its ownership record. When a request then woke it
on another node, its sockets on the first node closed only one by one,
each when its client next sent a message: a client that only listens
stayed connected and silently missed everything the room said from then
on. Now every socket of the room on that node closes with 1012 as soon
as one of them sends, and an owner sweep, once a minute, closes the
sockets of rooms another node took even if none of them sends. The
sweep reads at most 5,000 records a pass, counted in
wasmkeep_websocket_sweep_reads_total. A node's sockets also close with
1012 when its lease runs out (8 s without a write reaching the bucket,
with the defaults), before any other node can take its actors; nothing
closed them before. A drain closes the sockets of hibernated rooms, and
reportsdrainedonly once the node holds no socket. Each such close is
counted inwasmkeep_websocket_owner_closes_total
(running a cluster, monitoring). - A WebSocket the server closed stops at the close. Its client used to
keep it open by not answering the close, and its messages still ran
events; a node draining waited on it forever. Now nothing it sends after
the close is delivered, and it is dropped if its client has not answered
within 5 s. - An idle WebSocket is pinged after 25 s of silence from its client,
instead of 60 s, so a load balancer's usual idle timeout of 60 s no
longer cuts it first. The front door needs an idle timeout above 30 s
(front door). - A relayed WebSocket upgrade carries no
x-wasmkeep-*header from the
client. The relay to the actor's node removed a fixed list of node
headers; it now removes every header in that range, so a client cannot
pass a marker only nodes may set. - Take and alarm hand-off calls between nodes keep their own record of
used signatures, so a flood of other node calls that fills the shared
record (500,000 signatures at once) can no longer refuse the calls a
drain and a move depend on. The take and hand-off record holds 50,000.
Refusals because a record is full are counted in
wasmkeep_signatures_refused_full_totaland logged as a full record
rather than as a replay. - A database set aside as
<name>.db.replacedno longer has an older
WAL beside it. When a node replaces a local database with the bucket's
copy and keeps the old file, a replacement without a WAL left the
previous replacement's WAL next to the newer copy, where SQLite would
apply it if the copy were opened for recovery. The previous copy's WAL
and shared memory are now removed first. wasmkeep_admission_refused_total{reason}counts the requests,
WebSocket upgrades and actor calls a node refused before their event
ran:access,capacity,overloadedorunavailable
(monitoring).deployrefuses a second app of a tenant on a bucket prefix.
Actors are keyed by tenant, class and name, so a second app would share
the first app's actors without a word; until now only a node refused to
start, and only while a node of the other app was live. The first deploy
of an app now listsapps/<tenant>/and refuses when another app there
has acurrent, naming it and s...
WasmKeep v0.2.0
The installable release. With 0.1.0 the SDKs were used from a clone of
the repository; from this release each comes from where its language
expects it, and trying WasmKeep needs no toolchain. Still a pre-alpha
pre-release: for trying WasmKeep, side projects and staging, not
production. The interface between an actor and the host is unchanged
(wasmkeep:actor@0.2.0, a version of its own), so a component built for
0.1.0 runs as it is. See the roadmap for what comes next.
What is new
-
The SDKs install from their registries, each at the server's
version,0.2.0:Language Install Then Rust cargo add wasmkeep-sdkcrate, tutorial TypeScript npm install @wasmkeep/sdknpx wasmkeep-ts build(package)Python pip install wasmkeeppython -m wasmkeep build, new in this release (package)Go (TinyGo) go get github.com/wasmkeep/wasmkeep/sdks/go@v0.2.0module C, Zig wasmkeep-c-sdk-0.2.0.tar.gzandwasmkeep-zig-sdk-0.2.0.tar.gz, attached to this releaseC, Zig The crate, the npm package and the wheel are published from this
release's tag once a maintainer approves each, so a registry can be a
little behind this page. The server and every SDK carry one version;
use an SDK with the server of the same version
(versions). -
Prebuilt components: the release carries
counter.wasm,kv.wasm,
queue.wasm,workflows-orders.wasmandworkflows-ledger.wasm, built
from the examples and signed like its other files. The
quickstart is now a download and one command,
and the callers of the key-value, queue and
workflow classes in other
languages need no Rust toolchain. The notices of the libraries compiled
into them are inTHIRD_PARTY_NOTICES-components.txt. The binary's
tarball has a short README of its own. -
The C and Zig SDKs as archives, signed like the other files: each
SDK with the C bindings already generated, the WIT and the componentize
script, and a README whose build command works in the extracted
directory, without a clone or wit-bindgen.WASMKEEP_SDK_VERSION(C)
andwk.version(Zig) carry the version. -
A durable write on a cluster node is no longer synced to the local
disk as well as uploaded.exec-durableanswers once the bucket holds
the write, as it was documented to: one disk sync less on every durable
write. The bucket is the only durable copy of such a write, and a node
trusts a local file again only after checking it against the bucket. For
whoever runs a cluster: start a node again on the same bucket and
prefix, since after a crash or a power loss its local files may lack
writes the bucket has (replication). A
node without a bucket still syncs to its disk. -
Languages: Go through TinyGo moves from experimental to supported,
with a full Go SDK and its counter, chat room and jobs app. D
and MoonBit are experimental, each with an SDK, a counter and a chat
room; a C# counter (componentize-dotnet) and a standard-Go counter
(componentize-go) pass the shared tests.
Tiers and numbers. -
Compared with celld: the same counter on one node and on three, a
node killed under writes, with a check for lost writes that gives every
write a token of its own, repeats the takeovers with the killed node's
disk removed, and includes a run that must lose writes
(WasmKeep and celld,
benchmarks). -
How releases are made: a tag's release waits for CI to pass on the
tagged commit (0.1.0 was tagged before its run had finished), and a
version's image tag is written once. Publishing the release publishes
the SDKs to crates.io, npm and PyPI from its tag, without stored tokens,
each after a maintainer's approval (releasing).
Upgrading from 0.1.0
- Upgrade every node of a cluster together, as before
(upgrading). The formats in the
bucket and the command line are unchanged. - Components built for 0.1.0 need no rebuild. To move an actor's source
to a published SDK, replace the path into a clone with the registry's
package at0.2.0. - The Go module's
v0.1.0has only the key-value, queue and workflow
clients; thewasmkeeppackage (hooks, storage, alarms) starts at
v0.2.0.
Known limits
The limits listed for 0.1.0 still hold:
- Not production-ready: soaked against Cloudflare R2 and OVHcloud Object
Storage with nodes killed under load, nothing lost, but not yet carrying
real users' traffic, and not run against AWS S3
(measured). - Nodes of different versions are not supported in one cluster; one
tenant per process (security); no
independent third-party security audit yet
(security). - Binaries are for Linux; on macOS and Windows, use the image or build
from source. The Zig SDK is an archive, not yet a packagezig fetch
resolves; D and MoonBit are used from the source.
Install and verify
Install downloads a binary or runs the image.
Single node shows the
cosign check, the same for every file of the release;
Compose checks the image.
WasmKeep v0.1.0
The first release, published as a pre-alpha pre-release: for trying
WasmKeep, side projects and staging, not production. See the
roadmap for what comes next.
What is in it
- Actors: WebAssembly components (
wasmkeep:actor@0.2.0WIT world),
one instance per name, events one at a time, a SQLite database each,
hibernation, alarms that fire at least once, calls between actors,
WebSockets that stay open while an actor sleeps, outbound HTTP limited to
public addresses, and hot reload withwasmkeep dev. - Clusters: several nodes on one S3-compatible bucket with conditional
writes, each actor on one node at a time, its database replicated to the
bucket and restored on another node when its node dies (free 15 to about
21 s after the node's last write, then the restore). One lease per node,
checked in deterministic simulation. Mutual TLS between nodes, required
by default. - Operating a cluster:
wasmkeep deploy(rolling, with rollbacks,
access rules and configuration),wasmkeep cluster status,drain,
fenceandforget,wasmkeep actor move,wasmkeep secrets(sealed
in the bucket), garbage collection of old history in the bucket,
Prometheus metrics, health and readiness endpoints, and a Grafana
dashboard. - Languages: Rust (recommended); Zig and C/C++; TypeScript (QuickJS
by default, StarlingMonkey as an option) and Python; Go through TinyGo
(experimental). Tiers and numbers. - SDK patterns on actors: sharded key-value store, queues and durable
workflows (KV and queues,
workflows). - Packaging: binaries for x86_64 and aarch64 Linux (glibc 2.28 or
newer), each with a CycloneDX SBOM and third-party notices; a
distroless image for linux/amd64 and linux/arm64 at
ghcr.io/wasmkeep/wasmkeep:0.1.0; all signed with keyless cosign. A
three-node Compose example against MinIO in
deploy/compose/.
Known limits
- Not production-ready: soaked against Cloudflare R2 and OVHcloud Object
Storage with nodes killed under load, nothing lost, but not yet carrying
real users' traffic, and not run against AWS S3
(measured). - Upgrade every node of a cluster together: nodes of different versions
are not supported in one cluster
(upgrading). - One tenant per process (security).
- No independent third-party security audit yet
(security). - The performance targets on dedicated hardware are not measured yet;
millions of actors per cluster are a goal, not a measurement.
Install and verify
Single node checks the
binaries with cosign; Compose checks the image.