Skip to content

Split View: WASI 0.3의 네이티브 async — wasi:io가 사라진 이유, 그리고 아직 안 끝난 일

|

WASI 0.3의 네이티브 async — wasi:io가 사라진 이유, 그리고 아직 안 끝난 일

들어가며 — 6월 11일에 실제로 일어난 일

2026년 6월 11일, WASI 서브그룹이 투표로 WASI 0.3.0을 비준했습니다. 헤드라인은 "WebAssembly 컴포넌트에 async가 네이티브로 들어왔다"입니다.

이 문장은 하이프처럼 들리기 쉽고, 실제로 WASM 진영은 지난 몇 년간 "곧 도착한다"를 여러 번 반복했습니다. 그래서 이 글은 두 가지를 분리해서 보려고 합니다 — 실제로 스펙과 런타임에 들어간 것, 그리고 아직 안 끝났는데 끝난 것처럼 읽히기 쉬운 것.

먼저 결론부터. 이번 릴리스는 진짜입니다. 다만 "진짜"의 의미가 대부분의 사람이 기대하는 것과 다릅니다. 이건 성능 릴리스가 아니라 합성 가능성(composability) 릴리스입니다. 그리고 성능만 놓고 보면, 지금 이 순간에는 오히려 후퇴한 구간이 있습니다. 그 이야기까지 아래에서 하겠습니다.

WASI 0.2의 진짜 문제 — 샌드위치 문제

WASI 0.2에서도 async는 됐습니다. wasi:io 패키지가 pollable 리소스와 input-stream / output-stream, 그리고 poll 함수를 제공했고, 이걸로 논블로킹 I/O를 표현할 수 있었습니다.

문제는 컴포넌트를 조합할 때 터졌습니다. wasi.dev의 0.3 개요 문서가 드는 예가 명확합니다. 컴포넌트 A가 B를 부르고, B가 호스트를 부르는 상황을 생각해 봅시다.

A  →  B  →  Host

여기서 pollable단일 컴포넌트 인스턴스에 묶인 리소스입니다. 그래서 B는 호스트에게서 받은 pollable의 웨이크업을 A에게 그대로 전달할 방법이 없습니다. B가 할 수 있는 건 준비 신호를 중계하려고 자기가 계속 폴링하는 것뿐이고, 문서의 표현으로는 실무에서 그 웨이크업 사슬이 그냥 끊어집니다. 이걸 샌드위치 문제(sandwich problem)라고 부릅니다 — WASI 0.2는 async를 표현할 수는 있었지만, 컴포넌트 경계를 넘어 합성할 수는 없었습니다.

Bytecode Alliance의 발표 글은 이걸 더 직설적으로 정리합니다. WASI 0.2에서는 컴포넌트마다 자기 이벤트 루프 혹은 async 런타임이 필요했고, 그래서 개별 컴포넌트를 호스트 위에서 돌릴 수는 있었지만 그 이벤트 루프들끼리 서로 조율할 방법이 없었다는 것입니다. 결과적으로 컴포넌트가 스트리밍이나 async API를 쓰는 순간, 그 컴포넌트는 다른 컴포넌트와 조합될 수 없었습니다.

이게 왜 아픈지는 컴포넌트 모델의 존재 이유를 생각하면 분명합니다. 컴포넌트 모델의 세일즈 포인트가 "언어를 가리지 않고 조립할 수 있는 바이너리"인데, 실무 코드는 거의 다 I/O를 하고, I/O를 하면 async를 쓰고, async를 쓰면 조립이 안 됐습니다. 즉 가장 중요한 기능이 가장 흔한 경우에 작동하지 않았습니다.

WASI 0.3의 해법은 async를 컴포넌트 모델의 캐노니컬 ABI로 내리는 것입니다. 이제 이벤트 루프는 호스트가 하나만 관리하고, 모든 컴포넌트가 그걸 공유합니다. 스케줄링과 웨이크업 전파를 런타임이 소유하니까, 생산자와 소비자 사이에 컴포넌트가 몇 개 끼어 있든 async가 제대로 동작합니다.

세 가지 새 원시 타입

캐노니컬 ABI에 일급으로 추가된 건 세 가지입니다 — async func, 그리고 스트림과 퓨처 타입입니다.

// 함수 자체를 async로 선언한다
handle: async func(request: request) -> result<response, error-code>;

// stream<T>: 타입이 있는 비동기 데이터 채널
// future<T>: 단일 값의 비동기 완료
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;

몇 가지 설계 결정이 눈여겨볼 만합니다.

소유권. 스트림과 퓨처는 리소스 타입처럼 동작합니다 — 각각 소유된 핸들이고, 컴포넌트 경계를 넘겨주면 소유권이 호출자에서 피호출자로 넘어갑니다. 다만 리소스와 달리 빌려줄 수 없습니다(borrow 불가). 0.2의 input-stream이 인스턴스에 묶인 리소스여서 못 넘겼던 것과 정반대로, 이제 스트림은 넘길 수 있는 입니다. 샌드위치 문제가 풀리는 지점이 정확히 여기입니다.

완료 기반(completion-based) 모델. 이건 준비 기반(readiness-based)이 아닙니다. 발표 글은 이걸 리눅스의 io_uring, 윈도우의 IOCP/IoRing에 비유합니다. epoll이나 kqueue 스타일의 준비 기반 API가 필요하면 이 위에 에뮬레이션할 수 있다고 명시합니다. 이 선택은 취향 문제가 아니라 합성과 직결됩니다 — 준비 신호는 중계해야 하지만, 완료는 런타임이 직접 깨우면 그만이니까요.

스트림 상태 문제 해결. 0.2에서는 스트림의 종료 에러가 매 read 호출에 인라인으로 얹혀 나왔습니다. 그래서 호출자가 끝까지 읽어야만 결과를 알 수 있었고, 중간에 읽기를 멈추면 정상 종료와 에러를 구분할 수 없었습니다. 0.3은 스트림에 퓨처를 하나 더 딸려 보내서, 스트림을 얼마나 소비했는지와 무관하게 결과가 따로 확정되게 했습니다. 위 코드에서 스트림과 퓨처를 튜플로 함께 돌려주는 모양이 바로 그 답입니다.

wasi:io는 어디로 갔나

wasi:io 패키지는 통째로 삭제됐습니다. WASI 0.3 버전의 wasi:io는 존재하지 않습니다. 하던 일은 전부 컴포넌트 모델의 네이티브 원시 타입으로 흡수됐습니다.

릴리스 노트가 제시하는 매핑표가 이 릴리스의 절반을 요약합니다.

WASI 0.2 (wasi:io)              →  WASI 0.3 (Component Model)
--------------------------------------------------------------
resource pollable               →  future<T>
resource input-stream           →  stream<u8>
resource output-stream          →  stream<u8>  (쓰기 방향)
poll(list<pollable>)            →  future를 await (런타임이 처리)
subscribe() on resource         →  호출이 future<...>를 반환
start-foo / finish-foo          →  foo: async func(...)

마지막 줄이 실무에서 체감이 가장 큽니다. 0.2의 start-bind / finish-bind, start-connect / finish-connect 같은 2단계 춤과 그걸 굴리던 subscribe()가 전부 사라지고 async func 하나로 접힙니다.

인터페이스별로 실제로 바뀐 것

릴리스 노트는 대부분의 변경이 기계적이라고 말합니다. 0.2가 async를 흉내 내려고 부렸던 곡예가 필요 없어졌으니, 같은 걸 훨씬 간결하게 쓰게 됐다는 것입니다. 그래도 몇 군데는 기계적이지 않습니다.

wasi:http — 변경 폭이 가장 큽니다. 여기만은 폴링 인터페이스를 async로 기계 변환한 게 아니라 월드와 핵심 추상을 재조직했습니다. incoming-request, outgoing-request, incoming-response, outgoing-response, incoming-body, outgoing-body, future-trailers, future-incoming-response, response-outparam — 이 리소스들이 전부 사라지고, 바디가 스트림이고 트레일러가 퓨처인 통합된 request / response 리소스로 정리됐습니다. 그리고 월드가 둘로 나뉩니다.

// 서비스 월드: HTTP 호출을 하고, 들어오는 요청을 처리한다
world service {
  import client;
  export handler;
}

// 미들웨어 월드: service의 상위 집합
world middleware {
  include service;  // service가 하는 건 다 하고
  import handler;   // 들어온 요청을 다른 handler로 넘길 수도 있다
}

0.2 시절의 proxy 월드는 service로 대체됐고, middleware 월드는 요청 경로 미들웨어를 일급으로 표현합니다.

wasi:sockets — 인터페이스가 7개에서 2개로. network, instance-network, tcp, tcp-create-socket, udp, udp-create-sockettcp-socketudp-socket 리소스를 담은 통합 types 인터페이스로 합쳐지고, DNS는 별도 ip-name-lookup이 맡습니다. 그리고 network 리소스가 사라졌습니다 — 0.2는 네트워크 접근을 모든 bind/connect/lookup 호출에 실어 나르는 capability 리소스로 모델링했는데, 0.3은 이걸 없애고 월드 임포트 수준에서 권한을 줍니다.

wasi:clocks — 대부분 이름 바꾸기. wall-clocksystem-clock으로, datetimeinstant로 바뀌었습니다. 이유는 일관성입니다 — "wall clock"과 "datetime"은 POSIX에도, 러스트의 std::time에도 없는 WASI만의 용어였습니다. instant 레코드의 초는 u64에서 s64로 바뀌어 에포크 이전 타임스탬프를 표현할 수 있게 됐습니다. 다운스트림 영향은 대체로 찾아 바꾸기 수준이지만, 릴리스 노트가 굳이 이걸 플래그한 이유는 이 churn이 테스트 스위트 전반에 나타나기 때문입니다.

wasi:random — 이름 뒤에 의미 변화가 숨어 있습니다. get-random-bytes의 파라미터가 len에서 max-len으로 바뀌었는데, 이건 단순 리네임이 아닙니다. 구현이 요청한 것보다 적은 바이트를 반환해도 됩니다. 호출자가 원하는 만큼 모을 때까지 루프를 돌아야 한다는 뜻입니다. 기계적 변환이라고 믿고 넘어가면 물리기 좋은 지점입니다.

런타임은 준비됐나 — Wasmtime 46이 실제로 한 일

여기가 "약속"과 "출하"를 구분해야 하는 대목입니다.

발표 글(6월 11일)은 Wasmtime 45가 당시 최신 릴리스 후보를 돌리고 있고, Wasmtime 46이 컴포넌트 모델 async를 기본값으로 켜서 WASI 0.3.0을 출하할 것이라고 예고했습니다. 미래형입니다.

그래서 실제로 확인해 보면 — Wasmtime 46.0.0이 2026년 6월 22일에 릴리스됐고, 그 릴리스 노트에 이 줄이 있습니다: Wasmtime이 이제 WASI 0.3.0을 기본으로 지원하며 component-model-async wasm 기능이 기본 활성화됐다(#13612). 예고가 11일 만에 실제로 도착했습니다. 런타임 쪽은 진짜입니다.

한 가지 문서 불일치는 짚어 둘 만합니다. wasi.dev 로드맵은 "WASI 0.3 지원은 Wasmtime 43+에서 가능하다"고 적고 있는데, 발표 글은 45/46을 말합니다. 개요 문서를 보면 이 차이가 설명됩니다 — Wasmtime v44부터 wasmtime serve-Sp3 -W component-model-async=y 플래그를 주면 0.3 컴포넌트를 서빙할 수 있었고(그리고 0.3 service 월드를 익스포트하지 않는 컴포넌트는 자동으로 0.2의 wasi:http/proxy 월드로 폴백), v44는 wasi:tls@0.3.0-draft 초기 지원도 넣었습니다. 즉 플래그 뒤의 지원은 더 일찍 들어왔고, 기본값으로 켜진 건 46입니다. 문서마다 다른 숫자를 말하는 건 이 둘을 구분하지 않아서입니다.

적합성은 공용 wasi-testsuite로 검증하고, WASI 0.3 커버리지가 Wasmtime과 jco에서 리눅스·macOS·윈도우로 돌고 있습니다.

정직하게 — 아직 안 끝난 것들

여기서부터가 이 글을 쓴 이유입니다. 발표 글만 읽으면 다 끝난 것처럼 보이는데, Bytecode Alliance 자신이 낸 다른 글을 같이 읽으면 그림이 상당히 달라집니다.

동기 호출이 지금 약 3.5배 느립니다. The Road to Component Model 1.0에 이런 대목이 있습니다. 현재 구현은 컴포넌트 경계를 넘는 async 태스크 인프라를 호스트 콜로 관리하는데, 이게 순수하게 동기적인 호출 경로에도 대략 3.5배의 오버헤드를 얹습니다(닉 피츠제럴드가 Plumbers Summit에서 측정한 수치로 인용돼 있습니다). 스펙 수준에서 일부는 이미 처리됐고(P3 개발 중 recursion check가 제거됐습니다), 나머지는 WASI P3 출하 이후로 계획돼 있습니다 — 동기 어댑터가 스택에 가벼운 태스크만 할당하도록 태스크 상태를 리팩터링해서 Cranelift가 대부분 최적화로 날려 버릴 수 있게 하는 방향이고, 목표는 async 지원이 들어오기 전의 성능 프로파일을 되찾는 것입니다.

이 문장을 정확히 읽는 게 중요합니다. 목표가 "더 빨라지기"가 아니라 "예전만큼 돌아오기"입니다. 즉 async를 네이티브로 넣은 대가를 동기 경로가 지금 치르고 있고, 그 청구서는 아직 안 갚았습니다.

게스트 툴체인이 아직 따라오는 중입니다. 스펙이 비준됐다는 것과 당신 언어로 그걸 쓸 수 있다는 건 다른 얘기입니다. 발표 글은 파이썬, 자바스크립트, C#, C를 포함한 여러 언어에서 게스트 바인딩 생성기의 async 지원이 진행 중이라고 적습니다. jco는 WASI 0.3 전체를 지원하지만 그게 기본 활성화된 릴리스는 "곧" 나갈 예정이고, 러스트 쪽은 wit-bindgen으로 async fn이 자연스럽게 매핑됩니다. Go는 결이 다른데, componentize-go가 고루틴을 ABI 경계에서 파킹했다가 스트림이 준비되면 재개하는 식으로 스택풀 코루틴을 태웁니다 — 컴포넌트 모델의 async ABI가 스택리스와 스택풀 코루틴을 둘 다 수용하도록 설계됐기 때문에 가능한 일입니다. 발표 글의 표현대로 "앞으로 몇 주에서 몇 달에 걸쳐" 개별 프로젝트가 0.3 지원을 알릴 것입니다. 그 말은 오늘 기준으로는 대부분 아직이라는 뜻입니다.

스레드와 스트림 스플라이싱은 0.3.0에 못 들어갔습니다. 둘 다 0.3.x 후속 릴리스로 밀렸습니다. 협조적 스레드는 컴포넌트 모델 수준에 구현되고(WASI 아래층입니다), 러스트 컴포넌트가 std::thread::spawn을 쓰면 WIT에 아무것도 안 해도 지원을 받습니다. 진행 상황은 wasi-libc의 pthreads 지원이 거의 끝났고, LLVM 패치가 막 랜딩됐고, Wasmtime은 피처 플래그 뒤에 동작하는 구현이 있습니다. 선협조적, 후선점형 순서입니다. 스트림 스플라이싱(중간 복사 없이 스트림을 다른 스트림에 이어 붙이기)은 스트리밍 성능에 중요하지만, 문서의 표현으로는 그냥 마감에 너무 가까워서 P3 자체에는 못 실었습니다.

LLVM이 가장 긴 리드 타임일 수 있습니다. 1.0으로 가는 ABI 작업에는 C ABI 수준의 multivalue 반환이 묶여 있는데, LLVM이 아직 C ABI 레벨에서 multivalue를 지원하지 않습니다. 정확한 ABI 제안은 나와 있지만, 그걸 업스트림에 올리는 게 이 작업 중 가장 오래 걸릴 수 있다고 글이 직접 인정합니다. 참고로 LLVM은 6개월마다 릴리스하고, 러스트에 뭔가를 넣어 stable까지 가는 데 대략 9주가 걸립니다. 이 파이프라인 전체를 Bytecode Alliance가 통제하지 못합니다.

0.2에서 마이그레이션할 문서가 아직 없습니다. 실제 예제가 붙은 상세한 0.2 → 0.3 마이그레이션 가이드는 컴포넌트 모델 문서에 "준비 중"입니다.

"6자릿수 빨라진다"는 주장에 대하여

발표 글에 이런 문장이 있습니다. middleware 월드가 가능하게 하는 서비스 체이닝 덕분에, 서로 자주 통신하는 마이크로서비스 컴포넌트들이 네트워크를 타지 않고 같은 프로세스 안에서 직접 합성될 수 있고, 그래서 "대부분의 마이크로서비스에서 다른 마이크로서비스 호출 시간이 밀리초에서 나노초로, 6자릿수(six orders of magnitude) 줄어들 것"이라는 주장입니다.

이 문장은 이렇게 읽어야 합니다.

  • 이건 측정치가 아니라 전망입니다. 원문이 미래형("will reduce")이고, 벤치마크도 측정 조건도 제시되지 않습니다.
  • 비교 대상이 불공정합니다. "네트워크 홉"과 "같은 프로세스 안의 함수 호출"을 비교하면 당연히 자릿수가 갈립니다. 이건 WASI 0.3이 빨라서가 아니라 네트워크를 안 타서 나오는 숫자입니다. 같은 논리라면 두 마이크로서비스를 하나의 바이너리로 합치는 것도 6자릿수를 줍니다.
  • 그리고 바로 앞 절에서 봤듯, 같은 조직의 다른 글이 지금 동기 컴포넌트 간 호출에 3.5배 오버헤드가 있다고 적고 있습니다. 두 문장은 모순은 아닙니다(하나는 네트워크 제거 이득, 하나는 호출 규약 오버헤드) — 하지만 나란히 놓고 봐야 균형이 맞습니다.

브라우저 쪽 숫자도 같은 기준으로 읽는 게 좋습니다. 모질라의 라이언 헌트가 한 실험에서, DOM 변경이 많은 WASM VDOM 재조정 루프가 자바스크립트 글루 레이어를 우회해 브라우저 API를 직접 호출하면 2배에 가까운 속도 향상을 얻을 수 있었다고 합니다. 글 자체가 "실제 이득은 워크로드마다 다를 것"이라고 단서를 답니다. 특정 워크로드의 상한선이지, 일반적인 기대치가 아닙니다.

WASM 진영이 오래 시달려 온 게 정확히 이 지점입니다. 기술은 실제로 좋은데 주장이 늘 반 박자 앞서갑니다. 이번 릴리스의 진짜 성과는 나노초가 아니라 async를 쓰면서도 컴포넌트를 조합할 수 있게 된 것이고, 그것만으로 충분히 큰 뉴스입니다.

Component Model 1.0과 WASI 1.0 — 날짜는 없다

자주 헷갈리는 부분이라 정리하면, Component Model 1.0과 WASI 1.0은 별개의 마일스톤입니다. 그리고 순서가 있습니다 — WASI 1.0은 컴포넌트 모델이 먼저 1.0에 도달해야 그로부터 따라옵니다.

루크 와그너가 Plumbers Summit에서 쓴 비유가 이 관계를 잘 설명합니다. 컴포넌트 모델은 항상 존재하는 마이크로커널입니다 — 어떤 호스트에서든 돌아가는 기초 원시 타입을 제공합니다. WASI는 그 위에 얹히는 OS 서비스(네트워킹, 스토리지, 그래픽)이고, 마이크로커널 위의 프로세스처럼 돌며 기기에 따라 있을 수도 없을 수도 있습니다. 브라우저를 생각하면 이해가 쉽습니다 — 브라우저는 어떤 I/O API가 존재해야 하는지에 대해 아주 강한 의견이 있고, 그래서 WASI 인터페이스는 브라우저에서 폴리필로 돕니다. 반면 컴포넌트 모델 자체는 계산 원시 타입만 제공하므로 코어 WASM 옆에 네이티브로 구현될 수 있습니다.

그런데 여기에 만만치 않은 관문이 있습니다. 컴포넌트 모델은 최소 두 개의 브라우저 엔진에 네이티브로 구현되지 않으면 공식적으로 1.0에 도달할 수 없습니다. 현재 상태는 이렇습니다 — 모질라 팀이 최근 WebAssembly CG 대면 회의에서 성능 결과를 발표했고, 크롬 V8 팀은 컴포넌트 모델 구현 평가를 위한 이슈를 열었습니다. 글 자체의 표현이 정확합니다: 이건 약속이 아니라 의미 있는 신호입니다.

그래서 1.0을 얻어내려는 전략이 흥미롭습니다. asm.js 시절의 플레이북을 그대로 씁니다 — asm.js는 브라우저 엔진이 기능 사용 텔레메트리로 감지할 수 있는 no-op 문자열 "use asm"을 넣어 두고, 그 데이터로 최적화의 근거를 만들었고 결국 WebAssembly 자체로 이어졌습니다. jco가 지금 같은 걸 합니다. jco가 생성하는 자바스크립트 글루가 "use components" 식별자를 뱉습니다(jco 1.16.8에서 "use jco"에서 이름이 바뀌었습니다). jco로 트랜스파일된 컴포넌트의 실사용이 쌓이면, 그게 네이티브 구현의 근거가 된다는 계산입니다.

정리하면, WASI 1.0의 날짜를 말하는 1차 자료는 없습니다. 로드맵 페이지에도 없고, 1.0으로 가는 길을 다룬 글에도 없습니다. 로드맵 문서 자신이 첫 줄에 못을 박습니다 — 이건 살아 있는 문서이고, 목표와 전망은 잠정적이며 수정될 수 있다고. (인터넷에 도는 "2026년 말/2027년 초 목표" 같은 숫자를 몇 군데서 봤는데, 1차 자료에서 확인되지 않아 여기 옮기지 않습니다.)

확실한 건 케이던스뿐입니다. WASI 포인트 릴리스는 2개월마다, 해당 월의 첫 번째 목요일에, 릴리스 트레인 모델로 나갑니다 — "기차에 탈 준비가 된" 것이 무엇이든 상관없이 정해진 주기로 나간다는 뜻입니다. 0.3.0 이후로는 하위 호환되는 0.3.x가 이 기차를 탑니다. 예정된 화물은 언어 관용구에 통합된 취소(cancellation), 스트림 특수화와 최적화, 제로 카피를 넓히는 호출자 제공 버퍼, 그리고 스레드입니다.

그래서, 지금 옮겨야 하나

가장 중요한 사실부터. 마이그레이션은 필수가 아닙니다. 0.3 지원 런타임을 쓰기 위해 0.3으로 옮길 필요가 없습니다. wasmtime serve는 같은 바이너리로 0.3 컴포넌트와 0.2 컴포넌트를 컴포넌트별로 디스패치해서 둘 다 돌립니다. 로드맵도 구현체가 0.2와 0.3을 나란히 지원하거나, 0.2를 0.3 위에서 폴리필(가상화)할 수 있다고 명시합니다. P1 모듈도 아직 돌고, P2 컴포넌트도 아직 돕니다 — 이 팀은 P1 이래로 시맨틱 버저닝, 나란히 두는 구현, WASM-to-WASM 어댑터로 안정성을 유지해 왔고 그 이야기는 1.0 이후로도 계속됩니다.

지금 옮겨서 값을 하는 경우

  • 컴포넌트를 실제로 합성하고 있고, 그 사슬에 async나 스트리밍이 끼어 있다. 샌드위치 문제로 이미 아파 봤다면 이게 바로 그 약입니다.
  • HTTP 미들웨어를 컴포넌트로 표현하고 싶다 — 0.2의 proxy 월드로는 표현이 안 되던 게 middleware 월드로 일급이 됐습니다.
  • 스트림을 중간에 끊는 소비자가 있고, 정상 종료와 에러를 구분해야 한다.
  • 런타임을 Wasmtime 46+로 고정할 수 있고, 게스트 언어가 러스트다.

아직 기다리는 게 나은 경우

  • 동기 호출 경로가 핫패스다. 3.5배 오버헤드 수정이 아직 계획 단계입니다.
  • 게스트 언어가 파이썬·C#·C처럼 바인딩 async 지원이 진행 중인 쪽이다.
  • 스레드나 스트림 스플라이싱이 필요하다 — 0.3.x를 기다려야 합니다.
  • 단일 컴포넌트를 호스트 위에서만 돌린다. 합성을 안 하면 0.2로 충분하고, 0.3이 푸는 문제가 당신 문제가 아닙니다.

옮기기로 했다면 실무 함정이 하나 있습니다. 툴체인 버전을 일관되게 고정하세요. 컴포넌트 모델이 캐노니컬 인터페이스 이름을 정의해서 호환 버전 간 링크가 가능하긴 한데, 모든 도구가 아직 이 버전 인식 링킹을 지원하지 않습니다. 그때까지는 Wasmtime과 바인딩 생성기(러스트면 wit-bindgen, 자바스크립트면 jco)가 같은 WIT 버전 0.3.0을 타깃해야 합니다. 어긋나면 인스턴스화 시점에 알아보기 힘든 wrong type 에러로 튀어나옵니다. 이런 종류의 에러는 원인을 찾는 데 반나절이 사라지는 유형이라, 미리 알고 있는 값이 큽니다.

나머지 마이그레이션 절차는 대체로 기계적입니다 — wasi:io 타입을 위 매핑표대로 바꾸고, 월드를 적절히 고르고(CLI면 wasi:cli/command, HTTP 서버면 wasi:http/service, 미들웨어면 wasi:http/middleware), start-foo / finish-foo 호출 지점을 async func로 바꾸면 됩니다. 앞서 말한 wasi:randommax-len 같은 의미 변화만 기계적 변환에서 빠뜨리지 않으면 됩니다.

마치며

정리하면 이렇습니다. WASI 0.3.0은 2026년 6월 11일에 비준됐고, async를 wasi:io 패키지에서 컴포넌트 모델의 캐노니컬 ABI로 내렸습니다. wasi:io는 삭제됐고, pollable / input-stream / output-stream / pollasync func와 스트림·퓨처 원시 타입으로 대체됐습니다. Wasmtime 46이 6월 22일에 이걸 기본값으로 켰습니다.

이 릴리스의 값은 속도가 아니라 합성입니다. 0.2에서는 컴포넌트가 async를 쓰는 순간 조합 불가능해졌고 — 컴포넌트 모델의 존재 이유를 정면으로 갉아먹는 결함이었습니다 — 0.3은 이벤트 루프 소유권을 호스트로 옮겨 그걸 고쳤습니다. 그거면 충분히 큰 뉴스입니다. 나노초 마케팅은 없어도 됐습니다.

동시에 정직하게: 동기 호출 경로는 지금 약 3.5배 오버헤드를 지고 있고 그 수정은 아직 계획이며, 게스트 툴체인은 언어마다 진행 중이고, 스레드와 스플라이싱은 다음 기차이고, 컴포넌트 모델 1.0은 브라우저 엔진 두 곳의 네이티브 구현을 기다리고 있고, 아무도 WASI 1.0 날짜를 약속하지 않았습니다.

그래서 실무 판단은 단순합니다. 컴포넌트를 조합하고 있고 그 사슬에 async가 있다면 지금 보세요. 그게 아니라면 0.2는 계속 돌아가고, 폴리필도 있고, 다음 기차는 2개월 뒤에 옵니다. 이 프로젝트에서 가장 신뢰할 만한 건 로드맵의 날짜가 아니라 릴리스 트레인의 케이던스입니다 — 그리고 이번엔 예고한 것이 11일 만에 실제로 도착했다는 사실입니다.

참고 자료

Native Async in WASI 0.3 — Why wasi:io Disappeared, and What's Still Not Done

Introduction — What Actually Happened on June 11

On June 11, 2026, the WASI Subgroup voted to ratify WASI 0.3.0. The headline is: "async has arrived natively in WebAssembly components."

That sentence is easy to read as hype, and the WASM camp really has repeated "it's almost here" several times over the past few years. So this post tries to separate two things — what has actually landed in the spec and the runtime, and what isn't actually done yet but is easy to read as done.

Conclusion first. This release is real. But what "real" means here differs from what most people expect. This isn't a performance release — it's a composability release. And on performance alone, there's actually a spot right now that's a step backward. I'll get to that below too.

The Real Problem with WASI 0.2 — The Sandwich Problem

Async worked in WASI 0.2 too. The wasi:io package provided the pollable resource, input-stream / output-stream, and the poll function, and with those you could express non-blocking I/O.

The problem blew up when you composed components. The example wasi.dev's 0.3 overview doc gives is clear: consider a situation where component A calls B, and B calls the host.

A  →  B  →  Host

Here, pollable is a resource bound to a single component instance. So B has no way to hand the wakeup of a pollable it received from the host straight through to A. All B can do is keep polling itself to relay the readiness signal, and in the document's words, in practice that wakeup chain just breaks. This is called the sandwich problem — WASI 0.2 could express async, but it couldn't compose it across component boundaries.

The Bytecode Alliance's announcement post puts it more bluntly. In WASI 0.2, every component needed its own event loop or async runtime, so you could run individual components on top of a host, but those event loops had no way to coordinate with each other. As a result, the moment a component used streaming or an async API, that component couldn't be composed with other components.

Why this hurts becomes clear once you think about the reason the Component Model exists at all. The Component Model's selling point is "binaries you can assemble regardless of language," but real-world code almost always does I/O, doing I/O means using async, and using async meant you couldn't assemble it. In other words, the most important feature didn't work in the most common case.

WASI 0.3's solution is to push async down into the Component Model's canonical ABI. Now the host manages a single event loop, and every component shares it. Since the runtime owns scheduling and wakeup propagation, async works correctly no matter how many components sit between the producer and the consumer.

Three New Primitives

Three things were added to the canonical ABI as first-class citizens — async func, and the stream and future types.

// Declare the function itself as async
handle: async func(request: request) -> result<response, error-code>;

// stream<T>: a typed asynchronous data channel
// future<T>: asynchronous completion of a single value
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;

A few design decisions are worth a closer look.

Ownership. Streams and futures behave like resource types — each is an owned handle, and passing it across a component boundary transfers ownership from the caller to the callee. Unlike resources, though, they cannot be borrowed. In the exact opposite of 0.2, where input-stream was a resource bound to an instance and couldn't be handed off, streams are now values you can pass along. This is exactly where the sandwich problem gets solved.

A completion-based model. This is not readiness-based. The announcement post compares it to Linux's io_uring and Windows's IOCP/IoRing. It states explicitly that if you need a readiness-based API in the style of epoll or kqueue, you can emulate it on top of this. This choice isn't a matter of taste — it connects directly to composability, because readiness signals have to be relayed, while completions just need the runtime to wake things up directly.

Fixing the stream-state problem. In 0.2, a stream's termination error rode inline on every read call. So the caller could only find out the result by reading all the way to the end, and if you stopped reading partway through, you couldn't tell a clean finish apart from an error. 0.3 attaches one more future to the stream, so the result is settled separately, independent of how much of the stream you consumed. The shape in the code above — returning a stream and a future together as a tuple — is exactly that answer.

Where Did wasi:io Go

The wasi:io package was deleted entirely. There is no WASI 0.3 version of wasi:io. Everything it used to do has been absorbed into the Component Model's native primitives.

The mapping table the release notes give sums up half of this release.

WASI 0.2 (wasi:io)              →  WASI 0.3 (Component Model)
--------------------------------------------------------------
resource pollable               →  future<T>
resource input-stream           →  stream<u8>
resource output-stream          →  stream<u8>  (write direction)
poll(list<pollable>)            →  await the future (handled by the runtime)
subscribe() on resource         →  the call returns a future<...>
start-foo / finish-foo          →  foo: async func(...)

The last line is the one you feel most in practice. The two-step dance of 0.2's start-bind / finish-bind, start-connect / finish-connect, and the subscribe() that drove it, all disappear and collapse into a single async func.

What Actually Changed, Interface by Interface

The release notes say most of the changes are mechanical. The acrobatics 0.2 performed to fake async are no longer needed, so you now write the same thing far more concisely. Still, a few spots aren't mechanical.

wasi:http — the largest scope of change. This is the one place that didn't just mechanically convert a polling interface to async — it reorganized the worlds and the core abstractions. incoming-request, outgoing-request, incoming-response, outgoing-response, incoming-body, outgoing-body, future-trailers, future-incoming-response, response-outparam — all of these resources disappeared, consolidated into unified request / response resources where the body is a stream and the trailers are a future. And the world splits in two.

// service world: makes HTTP calls, handles incoming requests
world service {
  import client;
  export handler;
}

// middleware world: a superset of service
world middleware {
  include service;  // does everything service does
  import handler;   // can also forward incoming requests to another handler
}

The proxy world from the 0.2 era is replaced by service, and the middleware world expresses request-path middleware as a first-class citizen.

wasi:sockets — from 7 interfaces down to 2. network, instance-network, tcp, tcp-create-socket, udp, udp-create-socket merge into a unified types interface holding the tcp-socket and udp-socket resources, and DNS is handled by a separate ip-name-lookup. And the network resource is gone — 0.2 modeled network access as a capability resource carried along every bind/connect/lookup call, but 0.3 removes that and grants permission at the world-import level instead.

wasi:clocks — mostly renaming. wall-clock becomes system-clock, and datetime becomes instant. The reason is consistency — "wall clock" and "datetime" were WASI-only terms that appear in neither POSIX nor Rust's std::time. The seconds field of the instant record changed from u64 to s64, so it can now express pre-epoch timestamps. The downstream impact is mostly find-and-replace, but the release notes flag this specifically because the churn shows up across the whole test suite.

wasi:random — a semantic change hides behind the name. The parameter of get-random-bytes changed from len to max-len, and that isn't a simple rename. An implementation is now allowed to return fewer bytes than requested. That means callers have to loop until they've collected as many as they want. If you assume this is a mechanical conversion and move on, this is a good place to get bitten.

Is the Runtime Ready — What Wasmtime 46 Actually Did

This is the point where you have to separate "promise" from "ship."

The announcement post (June 11) said that Wasmtime 45 was running the then-latest release candidate, and that Wasmtime 46 would ship WASI 0.3.0 by turning Component Model async on by default. That's future tense.

So checking what actually happened — Wasmtime 46.0.0 was released on June 22, 2026, and its release notes have this line: Wasmtime now supports WASI 0.3.0 by default, and the component-model-async wasm feature is enabled by default (#13612). The promise arrived for real in 11 days. The runtime side is real.

One documentation inconsistency is worth pointing out. The wasi.dev roadmap states that "WASI 0.3 support is available from Wasmtime 43+," while the announcement post talks about 45/46. The overview doc explains the gap — from Wasmtime v44, wasmtime serve could serve 0.3 components if you passed the -Sp3 -W component-model-async=y flags (and a component that doesn't export the 0.3 service world automatically falls back to the 0.2 wasi:http/proxy world), and v44 also added initial support for wasi:tls@0.3.0-draft. In other words, support behind a flag landed earlier, and 46 is when it was turned on by default. Different documents cite different numbers because they don't distinguish between the two.

Conformance is verified with the shared wasi-testsuite, and WASI 0.3 coverage runs on Wasmtime and jco across Linux, macOS, and Windows.

Honestly — What's Still Not Done

This is where the actual reason for writing this post starts. Read only the announcement post and it looks like everything is done, but read it together with another post the Bytecode Alliance itself put out, and the picture changes considerably.

Synchronous calls are currently about 3.5x slower. The Road to Component Model 1.0 has this passage: the current implementation manages async task infrastructure that crosses component boundaries through host calls, and this adds roughly 3.5x overhead even to purely synchronous call paths (cited as a figure Nick Fitzgerald measured at the Plumbers Summit). Some of this has already been handled at the spec level (the recursion check was removed during P3 development), and the rest is planned for after WASI P3 ships — the direction is to refactor task state so the sync adapter allocates only a lightweight task on the stack, letting Cranelift optimize most of it away, with the goal of getting back to the performance profile from before async support was added.

It's important to read this sentence precisely. The goal isn't "get faster" — it's "get back to as fast as before." In other words, the synchronous path is currently paying the price of adding native async, and that bill hasn't been paid off yet.

Guest toolchains are still catching up. A spec being ratified and you actually being able to use it in your language are two different things. The announcement post says async support in guest binding generators is in progress across several languages, including Python, JavaScript, C#, and C. jco supports all of WASI 0.3, but a release where that's enabled by default is due "soon"; on the Rust side, async fn maps naturally via wit-bindgen. Go is a different texture — componentize-go runs stackful coroutines, parking goroutines at the ABI boundary and resuming them once a stream is ready. This is possible because the Component Model's async ABI is designed to accommodate both stackless and stackful coroutines. As the announcement post puts it, individual projects will announce 0.3 support "over the coming weeks to months." That means, as of today, most of them are still not there.

Threads and stream splicing didn't make it into 0.3.0. Both were pushed to a follow-up 0.3.x release. Cooperative threads are implemented at the Component Model level (a layer below WASI), and if a Rust component uses std::thread::spawn, it gets support without doing anything in WIT. Progress so far: wasi-libc's pthreads support is nearly done, an LLVM patch just landed, and Wasmtime has an implementation working behind a feature flag. The order is cooperative first, preemptive later. Stream splicing (stitching one stream into another without a copy in the middle) matters for streaming performance, but in the document's own words it simply came too close to the deadline to make it into P3 itself.

LLVM may be the longest lead time. The ABI work heading toward 1.0 is tied to C-ABI-level multivalue returns, and LLVM doesn't yet support multivalue at the C ABI level. A precise ABI proposal exists, but the post itself admits that landing it upstream could be the longest-running piece of this work. For reference, LLVM releases every six months, and it takes roughly nine weeks to get something into Rust all the way to stable. The Bytecode Alliance doesn't control this whole pipeline.

There's no migration documentation from 0.2 yet. A detailed 0.2 → 0.3 migration guide with real examples attached is "in progress" in the Component Model documentation.

On the Claim of "Six Orders of Magnitude Faster"

The announcement post has this line: thanks to the service chaining the middleware world enables, microservice components that talk to each other frequently can be composed directly inside the same process instead of going over the network, and so "for most microservices, the time to call another microservice will drop from milliseconds to nanoseconds — six orders of magnitude" is the claim.

This sentence should be read as follows.

  • This is a projection, not a measurement. The original text is future tense ("will reduce"), and no benchmark or measurement conditions are given.
  • The comparison is unfair. Comparing "a network hop" to "a function call inside the same process" is naturally going to split by orders of magnitude. This number comes from not going over the network, not from WASI 0.3 being fast. By the same logic, just merging two microservices into a single binary would also give you six orders of magnitude.
  • And as we saw in the section right before this, another post from the same organization currently states that there's a 3.5x overhead on calls between synchronous components. The two statements aren't contradictory (one is the gain from removing the network, the other is call-convention overhead) — but you need to place them side by side to get a balanced picture.

It's worth reading the browser-side number by the same standard. In an experiment by Mozilla's Ryan Hunt, a WASM VDOM reconciliation loop with heavy DOM changes was able to get close to a 2x speedup by bypassing the JavaScript glue layer and calling browser APIs directly. The post itself adds the caveat that "actual gains will vary by workload." It's an upper bound for a specific workload, not a general expectation.

This is exactly the spot the WASM camp has been dogged by for a long time. The technology is genuinely good, but the claims always run half a beat ahead. The real achievement of this release isn't nanoseconds — it's that components can now be composed while using async — and that alone is big enough news.

Component Model 1.0 and WASI 1.0 — No Date Yet

This is a spot people often confuse, so to be clear: Component Model 1.0 and WASI 1.0 are separate milestones. And there's an order — WASI 1.0 follows only after the Component Model reaches 1.0 first.

An analogy Luke Wagner made at the Plumbers Summit explains this relationship well. The Component Model is an always-present microkernel — it provides base primitives that run on any host. WASI is the OS services layered on top of that (networking, storage, graphics), running like processes on top of the microkernel, and it may or may not be present depending on the device. Thinking about the browser makes this easy to grasp — browsers have very strong opinions about which I/O APIs should exist, so WASI interfaces run as polyfills in the browser. The Component Model itself, by contrast, only provides computational primitives, so it can be implemented natively alongside core WASM.

But there's a formidable gate here. The Component Model cannot officially reach 1.0 unless it's natively implemented in at least two browser engines. Here's the current state — the Mozilla team recently presented performance results at a WebAssembly CG in-person meeting, and the Chrome V8 team opened an issue to evaluate a Component Model implementation. The post's own wording is precise: this is a meaningful signal, not a promise.

So the strategy for getting to 1.0 is interesting. It uses the exact playbook from the asm.js days — asm.js embedded a no-op string, "use asm", that browser engines could detect through feature-usage telemetry, and that data built the case for optimization, eventually leading to WebAssembly itself. jco is doing the same thing now. The JavaScript glue jco generates emits a "use components" identifier (renamed from "use jco" in jco 1.16.8). The calculation is that as real-world usage of jco-transpiled components accumulates, it builds the case for a native implementation.

To sum up, there is no primary source that states a date for WASI 1.0. Not on the roadmap page, and not in the post covering the road to 1.0. The roadmap document itself nails this down in its first line — that it's a living document, and its goals and projections are tentative and subject to change. (I've seen numbers like a "late 2026 / early 2027 target" circulating online in a few places, but since they're not confirmed in any primary source, I won't carry them over here.)

The only thing that's certain is the cadence. WASI point releases go out every two months, on the first Thursday of that month, on a release-train model — meaning it ships on a fixed cadence regardless of whatever happens to be "ready to board." After 0.3.0, backward-compatible 0.3.x releases ride this train. The cargo on the schedule: cancellation integrated into language idioms, stream specialization and optimization, caller-provided buffers that widen zero-copy, and threads.

So, Should You Migrate Now

The most important fact first. Migration is not mandatory. You don't have to move to 0.3 just to use a 0.3-capable runtime. wasmtime serve runs both 0.3 and 0.2 components from the same binary, dispatching per component. The roadmap also states explicitly that implementations can support 0.2 and 0.3 side by side, or polyfill 0.2 on top of 0.3 (virtualize it). P1 modules still run, and P2 components still run too — this team has maintained stability since P1 through semantic versioning, side-by-side implementations, and WASM-to-WASM adapters, and that story continues past 1.0 as well.

When migrating now pays off

  • You're actually composing components, and async or streaming is part of that chain. If you've already felt the pain of the sandwich problem, this is exactly the cure.
  • You want to express HTTP middleware as a component — something that couldn't be expressed with 0.2's proxy world is now first-class with the middleware world.
  • You have consumers that stop a stream partway through, and you need to distinguish a clean finish from an error.
  • You can pin your runtime to Wasmtime 46+, and your guest language is Rust.

When it's better to wait

  • The synchronous call path is your hot path. The fix for the 3.5x overhead is still only at the planning stage.
  • Your guest language is one like Python, C#, or C, where binding async support is still in progress.
  • You need threads or stream splicing — you'll have to wait for 0.3.x.
  • You only run a single component on top of a host. If you're not composing, 0.2 is enough, and the problem 0.3 solves isn't your problem.

If you decide to migrate, there's one practical trap. Pin your toolchain versions consistently. The Component Model defines canonical interface names, which does make linking across compatible versions possible, but not every tool supports this version-aware linking yet. Until they do, Wasmtime and your binding generator (wit-bindgen for Rust, jco for JavaScript) need to target the same WIT version, 0.3.0. If they drift apart, it surfaces at instantiation time as a hard-to-diagnose wrong type error. This is the kind of error that eats half a day tracking down the cause, so knowing about it ahead of time is worth a lot.

The rest of the migration procedure is mostly mechanical — swap wasi:io types according to the mapping table above, choose the right world (wasi:cli/command for a CLI, wasi:http/service for an HTTP server, wasi:http/middleware for middleware), and change start-foo / finish-foo call sites to async func. Just don't let semantic changes like the max-len one in wasi:random mentioned earlier slip through your mechanical conversion.

Closing

To sum up: WASI 0.3.0 was ratified on June 11, 2026, moving async out of the wasi:io package and down into the Component Model's canonical ABI. wasi:io was deleted, and pollable / input-stream / output-stream / poll were replaced by async func and the stream/future primitives. Wasmtime 46 turned this on by default on June 22.

This release's value isn't speed — it's composability. Under 0.2, a component became impossible to compose the moment it used async — a defect that ate directly into the Component Model's reason for existing — and 0.3 fixed that by moving event-loop ownership to the host. That alone is big enough news. The nanosecond marketing wasn't even necessary.

At the same time, to be honest: the synchronous call path currently carries roughly 3.5x overhead and the fix for it is still just a plan; guest toolchains are in progress across languages; threads and splicing are on the next train; Component Model 1.0 is waiting on native implementations in two browser engines; and nobody has promised a WASI 1.0 date.

So the practical call is simple. If you're composing components and async is part of that chain, look at it now. If not, 0.2 keeps running, a polyfill exists, and the next train comes in two months. The most trustworthy thing about this project isn't a date on the roadmap — it's the cadence of the release train. And this time, what was promised actually arrived in 11 days.

References