Skip to content

Commit e76181b

Browse files
Merge pull request #198 from spinframework/spin4-blog
Announcing Spin v4.0
2 parents 4c7c7d9 + ef70b5f commit e76181b

1 file changed

Lines changed: 366 additions & 0 deletions

File tree

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
title = "Announcing Spin v4.0"
2+
date = "2026-06-15T17:00:00Z"
3+
template = "blog_post"
4+
description = "Announcing Spin v4.0: stabilized WASIp3 support, async host APIs across the board, build profiles, and fine-grained capability inheritance for dependencies."
5+
tags = []
6+
7+
[extra]
8+
type = "post"
9+
author = "The Spin Project"
10+
11+
---
12+
13+
The CNCF Spin project just released [Spin v4.0.0](https://github.com/spinframework/spin/releases/tag/v4.0.0), the first major release of Spin in over a year. This release delivers a production-grade, stable implementation of WASI Preview 3; rewrites Spin's host APIs around `async`; adds build profiles; and introduces fine-grained capability inheritance for component dependencies.
14+
15+
There's a lot in this release, so this post is part release-notes and part tutorial. We'll walk through each headline feature with a working example.
16+
17+
- [WASI Preview 3: stabilized and supported long-term](#wasip3-stabilized-and-supported-long-term)
18+
- [Async everywhere: Spin's host interfaces are now async](#async-everywhere-spins-host-interfaces-are-now-async)
19+
- [Build profiles](#build-profiles)
20+
- [Fine-grained capability inheritance for dependencies](#fine-grained-capability-inheritance-for-dependencies)
21+
- [Targeting a deployment environment](#targeting-a-deployment-environment)
22+
- [Shell completions](#shell-completions)
23+
- [Upgrading to Spin 4.0](#upgrading-to-spin-40)
24+
25+
## WASI Preview 3: stabilized and supported long-term
26+
27+
WASI Preview 3 (WASIp3) is the next major revision of the WebAssembly System Interface. It brings first-class async, concurrent component exports, and significantly simpler WIT definitions to the component model. In practice, that means your components can handle multiple in-flight requests on a single instance, fan out concurrent I/O with plain `await`, and talk to host interfaces using idiomatic async code in each language.
28+
29+
**Spin 4.0 ships with the March 2026 release candidate of WASIp3, and we are committing to supporting it long-term.** WASIp3 is now the default platform for new applications, and the Spin Rust, Python, and Go SDKs have all been updated to use it.
30+
31+
If you followed along in [Spin 3.5](https://spinframework.dev/blog/announcing-spin-3-5) and [Spin 3.6](https://spinframework.dev/blog/announcing-spin-3-6), you've seen WASIp3 progress from "experimental, opt-in, might break between releases" to something ready for production use.
32+
33+
What this means in practice:
34+
35+
- **Concurrent, async component exports.** A single component instance can service multiple in-flight requests concurrently, instead of one instance per request.
36+
- **One idiomatic story per language.** Rust handlers are `async fn` using `http` / `hyper` types, and Python handlers are `async def`. Go handlers remain standard `net/http`, but now build with the standard Go toolchain (see below).
37+
- **WASIp2 components continue to run unchanged.** The HTTP trigger speaks WASIp3 natively, and existing WASIp2 components keep working without changes.
38+
39+
Here's the new minimum-viable Rust HTTP component in 4.0:
40+
41+
```rust
42+
use spin_sdk::http::{IntoResponse, Request, Response};
43+
use spin_sdk::http_service;
44+
45+
#[http_service]
46+
async fn handle(_req: Request) -> anyhow::Result<impl IntoResponse> {
47+
Ok(Response::builder()
48+
.status(200)
49+
.header("content-type", "text/plain")
50+
.body("Hello, Spin 4!".to_string())?)
51+
}
52+
```
53+
54+
A few things to notice:
55+
56+
- The handler is `async fn`. That's not cosmetic, it's a real WASIp3 async export. While this handler is awaiting I/O, Spin can dispatch another request into the same component instance.
57+
- `Request` and `Response` are re-exports from the ecosystem `http` crate, so this code composes with Axum, Tower, `http-body-util`, and friends with no glue types.
58+
59+
For a richer illustration of concurrent async exports, see the [gRPC sample](https://github.com/spinframework/spin-rust-sdk/tree/main/examples/grpc) in the Spin repo.
60+
61+
### Python and Go
62+
63+
The same story holds in Python and Go. The Python SDK exposes a single `handle_request` coroutine:
64+
65+
```python
66+
from spin_sdk.http import Handler, Request, Response
67+
68+
class HttpHandler(Handler):
69+
async def handle_request(self, request: Request) -> Response:
70+
return Response(
71+
200,
72+
{"content-type": "text/plain"},
73+
bytes("Hello from Python on WASIp3!", "utf-8"),
74+
)
75+
```
76+
77+
Build it with:
78+
79+
```bash
80+
componentize-py -w spin:up/http-trigger@4.0.0 componentize app -o app.wasm
81+
```
82+
83+
### No more TinyGo
84+
85+
On the Go side, the 4.0 release lines up with Go SDK `v3`, which **drops the TinyGo requirement**. Spin Go components now build with the standard Go toolchain (Go 1.25.5+) via `componentize-go`:
86+
87+
```go
88+
package main
89+
90+
import (
91+
"fmt"
92+
"net/http"
93+
94+
spinhttp "github.com/spinframework/spin-go-sdk/v3/http"
95+
)
96+
97+
func init() {
98+
spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
99+
w.Header().Set("content-type", "text/plain")
100+
fmt.Fprintln(w, "Hello from Go on WASIp3!")
101+
})
102+
}
103+
104+
func main() {}
105+
```
106+
107+
```toml
108+
[component.hello-go.build]
109+
command = "go tool componentize-go build"
110+
```
111+
112+
If you've been writing Spin Go components against `spin-go-sdk/v2` with TinyGo, this is the big one: standard Go, and no `tinygo build -target=wasip1 -gc=leaking ...` incantation.
113+
114+
### Concurrent outbound HTTP: the canonical demo
115+
116+
Because component exports are async, you can now fan out concurrent I/O from inside a component. The old ceremony of spinning up an async runtime by hand is gone, just `await` futures:
117+
118+
```rust
119+
use futures::future::{select, Either};
120+
use spin_sdk::http::{EmptyBody, IntoResponse, Request, send};
121+
use spin_sdk::http_service;
122+
use std::pin::pin;
123+
124+
#[http_service]
125+
async fn handle(_req: Request) -> anyhow::Result<impl IntoResponse> {
126+
let spin = pin!(content_length("https://spinframework.dev"));
127+
let book = pin!(content_length("https://component-model.bytecodealliance.org/"));
128+
129+
let (winner, len) = match select(spin, book).await {
130+
Either::Left((len, _)) => ("Spin docs", len?),
131+
Either::Right((len, _)) => ("Component model book", len?),
132+
};
133+
134+
Ok(format!("{winner} responded first, content-length = {len:?}\n"))
135+
}
136+
137+
async fn content_length(url: &str) -> anyhow::Result<Option<u64>> {
138+
let req = Request::get(url).body(EmptyBody::new())?;
139+
let res = send(req).await?;
140+
Ok(res
141+
.headers()
142+
.get("content-length")
143+
.and_then(|v| v.to_str().ok())
144+
.and_then(|v| v.parse().ok()))
145+
}
146+
```
147+
148+
Both requests are in flight at once, inside a single Wasm instance, scheduled by Spin. That same instance may also be serving other requests concurrently. And because WASIp3 supports streaming response bodies, a handler can start writing its response before it's finished computing the rest, something we'll use in the next section.
149+
150+
### Heads up on global state
151+
152+
> Because a single instance can now serve multiple concurrent requests, instance-scoped "global" state is shared across in-flight requests. This is the same rule you already follow in Axum, Express, or Flask, but it's a genuine shift from "one instance per request" if you've been writing Spin components against WASIp2 semantics. Audit any `static`, module-level, or `OnceCell` state and reach for per-request state or explicit synchronization where needed.
153+
154+
## Async everywhere: Spin's host interfaces are now async
155+
156+
WASIp3 unlocks async, but the benefit only lands if the *host APIs* your component calls are also async. A lot of Spin 4.0's work happened here: we've asyncified Spin's host interfaces so I/O-heavy handlers actually get concurrency instead of blocking the instance.
157+
158+
In 4.0, the following interfaces are async from the guest's perspective:
159+
160+
- **Key-value**: `Store::open_default().await`, `store.get(key).await`, `store.set(key, value).await`
161+
- **SQLite**: `Connection::open_default().await`, `connection.execute(...).await`
162+
- **PostgreSQL**: `Connection::open(...).await`, `connection.query(...).await`
163+
- **Redis (outbound and trigger)**: `Connection::open(...).await`, plus async Redis subscriber handlers
164+
- **Outbound HTTP**: `spin_sdk::http::send(req).await`
165+
166+
Here's a Rust handler that runs a SQLite query and streams each row to the client as it arrives, using `spin_sdk::wasip3::spawn`:
167+
168+
```rust
169+
use bytes::Bytes;
170+
use futures::{SinkExt, StreamExt};
171+
use http_body_util::StreamBody;
172+
use spin_sdk::http::{IntoResponse, Request, Response};
173+
use spin_sdk::http_service;
174+
use spin_sdk::sqlite::Connection;
175+
176+
#[http_service]
177+
async fn handle(_req: Request) -> anyhow::Result<impl IntoResponse> {
178+
let db = Connection::open_default().await?;
179+
180+
// A channel the spawned task will push body chunks into.
181+
let (mut tx, rx) = futures::channel::mpsc::channel::<Bytes>(1024);
182+
let rx = rx.map(|value| anyhow::Ok(http_body::Frame::data(value)));
183+
let response = Response::builder()
184+
.header("content-type", "application/x-ndjson")
185+
.body(StreamBody::new(rx))?;
186+
187+
// Spawn a background Wasm task. It outlives `handle` returning.
188+
spin_sdk::wasip3::spawn(async move {
189+
let mut query_result = db
190+
.execute("SELECT id, name FROM users ORDER BY id", [])
191+
.await?;
192+
let id_idx = query_result.columns().iter().position(|c| c == "id").unwrap();
193+
let name_idx = query_result.columns().iter().position(|c| c == "name").unwrap();
194+
195+
while let Some(row) = query_result.next().await {
196+
let id: i64 = row.get(id_idx).unwrap();
197+
let name: &str = row.get(name_idx).unwrap();
198+
let line = format!("{{\"id\":{id},\"name\":\"{name}\"}}\n");
199+
let _ = tx.send(line.into()).await;
200+
}
201+
query_result.result().await?;
202+
// Dropping `tx` closes the body stream.
203+
anyhow::Ok(())
204+
});
205+
206+
Ok(response)
207+
}
208+
```
209+
210+
Spin starts streaming the response as soon as `handle` returns, and each row hits the client as soon as SQLite yields it, without buffering the full result set in memory. That's a real win for time-to-first-byte and for large queries: you don't have to wait for every row before the client starts receiving bytes.
211+
212+
The same pattern is available in Python (via `componentize_py_async_support.spawn`) and Go (via plain `go func() { ... }()` goroutines).
213+
214+
## Build profiles
215+
216+
Development tools often offer ways to build code in different ways for different purposes: for example, debug builds that favor diagnostic value over speed and size, or profiling builds that inject performance instrumentation. Spin 4.0 introduces named **build profiles**, so you can leverage those options in Wasm applications, for example, to define a debug or profile build of your application.
217+
218+
You declare alternate profiles inline under each component, and select one at the command line:
219+
220+
```toml
221+
spin_manifest_version = 2
222+
223+
[application]
224+
name = "sentiment-analysis"
225+
226+
[component.sentiment-analysis]
227+
source = "target/spin-http-js.wasm"
228+
229+
[component.sentiment-analysis.build]
230+
command = "npm run build"
231+
watch = ["src/**/*", "package.json", "package-lock.json"]
232+
233+
# A `debug` profile for the sentiment-analysis component.
234+
[component.sentiment-analysis.profile.debug]
235+
source = "target/spin-http-js.debug.wasm"
236+
237+
[component.sentiment-analysis.profile.debug.build]
238+
command = "npm run build:debug"
239+
240+
# The `ui` component has no debug profile, it'll fall back to the default.
241+
[component.ui]
242+
source = { url = "https://.../spin_static_fs.wasm", digest = "sha256:..." }
243+
244+
# The `kv-explorer` component pulls a pre-built debug build from a registry.
245+
[component.kv-explorer]
246+
source = { url = "https://.../spin-kv-explorer.wasm", digest = "sha256:..." }
247+
248+
[component.kv-explorer.profile.debug]
249+
source = { url = "https://.../spin-kv-explorer.debug.wasm", digest = "sha256:..." }
250+
```
251+
252+
Now you can run the same manifest in two modes:
253+
254+
```bash
255+
# Production: every component uses its default build.
256+
$ spin up
257+
258+
# Debug: every component that defines `debug` uses it; others fall back.
259+
$ spin up --profile debug
260+
```
261+
262+
Learn more in the [Spin docs on build profiles](https://spinframework.dev/v4/build#building-with-profiles).
263+
264+
## Fine-grained capability inheritance for dependencies
265+
266+
Spin 3 introduced component dependencies, which let you use off-the-shelf Wasm components as libraries without having to write composition scripts or commands. Once you start using component dependencies like that, a natural concern is what capabilities those dependencies have, your application needs to connect to your database, but your regex matcher sure doesn't.
267+
268+
Spin 3 offered a partial solution where dependencies could be fully isolated, and Spin 4 improves on this by letting you manage dependency capabilities more selectively, on a dependency-by-dependency, capability-by-capability basis. This is the principle of least privilege, expressed in `spin.toml`, and it means you can hand out dependencies from across your organization, or from the registry, and grant them exactly the capabilities they need, and nothing more.
269+
270+
Concretely, Spin 4 replaces the coarse `dependencies_inherit_configuration` toggle with a per-dependency `inherit_configuration` field that accepts three forms.
271+
272+
**1. Inherit everything** — the dependency has access to all the capabilities of the main component:
273+
274+
```toml
275+
[component."infra-dashboard".dependencies]
276+
"acme:s3-client" = { version = "1.0.0", inherit_configuration = true }
277+
```
278+
279+
This is similar to `dependencies_inherit_configuration = true` in Spin 3, but scoped to just this dependency.
280+
281+
**2. Inherit nothing** (the default):
282+
283+
```toml
284+
[component."infra-dashboard".dependencies]
285+
"acme:s3-client" = { version = "1.0.0", inherit_configuration = false }
286+
```
287+
288+
The dependency is fully isolated from the parent's configuration, any capability import the component calls will return an error. Per-dependency isolation like this is itself new in Spin 4.
289+
290+
**3. Inherit a specific subset.** This is the new power:
291+
292+
```toml
293+
[component."infra-dashboard"]
294+
allowed_outbound_hosts = ["https://s3.us-west-2.amazonaws.com"]
295+
key_value_stores = ["my-key-value-cache"]
296+
297+
[component."infra-dashboard".dependencies]
298+
"acme:s3-client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] }
299+
```
300+
301+
Here `acme:s3-client` can make outbound HTTPS calls to the parent's allowed hosts, enough to reach S3. But every other capability is denied. Specifically, the S3 client *cannot* see `my-key-value-cache`.
302+
303+
For the full list of configuration keys you can inherit and how they map to WIT interfaces, see the [Spin docs on component dependencies](https://spinframework.dev/v4/writing-apps#using-component-dependencies).
304+
305+
## Targeting a deployment environment
306+
307+
Some Spin platforms ship custom templates and plugins tailored to that platform, for example, templates that use only the APIs available in the platform, plus the deployment plugin you need to ship to it. Spin 4.0 makes targeting one of these environments a single step with the new `-E` flag:
308+
309+
```bash
310+
$ spin new -E <environment>
311+
```
312+
313+
You don't need to pre-install anything: Spin fetches the environment's templates on demand, and installs any plugins the environment requires (typically a deployment plugin) at the same time. Your platform's documentation will tell you the `environment` name to use.
314+
315+
The same `-E` flag works when managing plugins directly, so you can list or install just the plugins associated with an environment:
316+
317+
```bash
318+
$ spin plugins list -E <environment>
319+
$ spin plugins install -E <environment>
320+
```
321+
322+
Learn more in the docs on [creating an application for a specific deployment environment](https://spinframework.dev/v4/writing-apps#creating-an-application-for-a-specific-deployment-environment) and [installing plugins for a specific deployment environment](https://spinframework.dev/v4/managing-plugins#installing-plugins-for-a-specific-deployment-environment).
323+
324+
## Shell completions
325+
326+
Spin 4.0 can now generate shell command completions for bash and zsh. To enable them, run the following during shell startup (for example, in your `.bashrc`):
327+
328+
```bash
329+
source <(COMPLETE=bash spin maintenance generate-completions)
330+
```
331+
332+
For zsh, change the `COMPLETE` variable to `zsh`.
333+
334+
Shell completions are a work in progress, and have a few known limitations:
335+
336+
- You do not get trigger option completions in `spin up` (which, unfortunately, includes a lot of options).
337+
- You do not get `spin up` option completions on `spin build --up` or `spin watch`.
338+
- You do not get completions for plugin commands.
339+
340+
See the [installation docs](https://spinframework.dev/v4/install#shell-completions) for more detail.
341+
342+
## Upgrading to Spin 4.0
343+
344+
1. **Install Spin 4.0** from [spinframework.dev/install](https://spinframework.dev/install) or grab a binary from the [release page](https://github.com/spinframework/spin/releases/tag/v4.0.0).
345+
2. **Update your templates:**
346+
```bash
347+
spin templates install --git https://github.com/spinframework/spin --update
348+
```
349+
3. **Update the SDK** your component uses:
350+
- Rust: `spin-sdk = "6.0"` and target `wasm32-wasip2` (WASIp2 and WASIp3 share the same binary target).
351+
- Python: pull the latest `spin-sdk` and `componentize-py` into your virtualenv.
352+
- Go: switch to `github.com/spinframework/spin-go-sdk/v3` and use Go 1.25.5+ with `go tool componentize-go build`.
353+
4. **Remove** any `executor = { type = "wasip3-unstable" }` lines from your manifest, they're no longer needed.
354+
5. **Audit instance-scoped state.** Concurrent in-flight requests on a single instance is now the default.
355+
356+
For a full walkthrough, see the [v4 quickstart](https://spinframework.dev/v4/quickstart) and the updated [language guides](https://spinframework.dev/v4/language-support-overview).
357+
358+
## Thank you
359+
360+
Spin 4.0 is the work of a lot of people across a lot of organizations, contributors to Spin itself, to `wasmtime`, to `wit-bindgen`, to `componentize-py`, to the Spin Go SDK, and to the WASIp3 standardization effort in the Bytecode Alliance. Thank you, and thank you to the CNCF for continuing to support the project.
361+
362+
## Stay in touch
363+
364+
Join us at weekly [project meetings](https://github.com/spinframework/spin#getting-involved-and-contributing), say hi on the [Spin CNCF Slack channel](https://cloud-native.slack.com/archives/C089NJ9G1V0), and follow [@spinframework](https://twitter.com/spinframework) on X.
365+
366+
Ready to build? Head to the [Spin quickstart](https://spinframework.dev/v4/quickstart), or browse the [Spin Hub](https://spinframework.dev/hub) for inspiration.

0 commit comments

Comments
 (0)