---
title: "Callouts and shared state"
url: "/docs/extend/callouts-and-shared-state/"
kind: "feature"
summary: "Async HTTP and gRPC callouts, timers, and host-backed shared memory for WASM filters: atomics and versioned compare-and-swap blobs."
---

> Arion site index: [/llms.txt](/llms.txt)

# Callouts and shared state

Async HTTP and gRPC callouts, timers, and host-backed shared memory for WASM filters: atomics and versioned compare-and-swap blobs.

The SDK's host calls let a WASM filter reach outside its sandbox: dispatch HTTP and gRPC requests to upstream clusters, sleep without blocking a worker, and share state across plugin instances. Use them when a filter needs an external answer — a token check, a dynamic allowlist — but the filter logic itself should stay in-process.

## HTTP callouts

`dispatch_http_call` sends a request to a named [cluster](@/docs/traffic/upstreams-and-load-balancing.md) from your configuration and returns the response. The call looks blocking in plugin code, but the module is suspended while it waits — proxy worker threads keep serving other traffic.

```rust
use arion_wasm_sdk::{bytes, dispatch_http_call, http};

let req = http::Request::builder()
    .method(http::Method::POST)
    .uri("/verify")
    .body(bytes::Bytes::from("token=xyz"))
    .unwrap();

match dispatch_http_call("auth", req) {
    Ok(resp) if resp.status().is_success() => FilterAction::Continue,
    _ => ctx.direct_response(deny_response()),
}
```

## gRPC callouts

`dispatch_grpc_call` speaks native gRPC to a cluster. You name the service and method, attach metadata, and pass the encoded protobuf message; the response carries the payload, metadata, and gRPC status (0 is OK). This needs the `arion-wasm-types` and `smol_str` crates in your plugin's dependencies.

```rust
use arion_wasm_sdk::dispatch_grpc_call;
use arion_wasm_types::GrpcCalloutRequest;
use smol_str::SmolStr;

let call = GrpcCalloutRequest {
    cluster_name: SmolStr::new("auth"),
    service_name: SmolStr::new("auth.AuthService"),
    method_name: SmolStr::new("VerifyToken"),
    initial_metadata: vec![],
    message: encoded_request,
};

match dispatch_grpc_call(&call) {
    Ok(resp) if resp.status == 0 => FilterAction::Continue,
    _ => ctx.direct_response(deny_response()),
}
```

## Timers and deadlines

`sleep(Duration)` suspends the module without blocking the worker thread. `set_io_timeout(Duration)` arms a deadline for the callouts that follow: a callout that overruns it returns `Err(ArionWasmError::Timeout)` instead of hanging the request. Disarm it with `clear_io_timeout()` when the I/O is done.

```rust
use std::time::Duration;
use arion_wasm_sdk::{clear_io_timeout, set_io_timeout, ArionWasmError};

let _ = set_io_timeout(Duration::from_secs(2));
match dispatch_http_call("auth", req) {
    Err(ArionWasmError::Timeout) => return ctx.direct_response(deny_response()),
    other => handle(other),
}
let _ = clear_io_timeout();
```

## Shared state across instances

Each in-flight request runs its own plugin instance, so struct fields are request-local. For state that must survive across requests and workers, the host keeps named variables shared by every instance of the same configured filter (up to 1024 names per filter), in two shapes.

**Atomics.** `SharedAtomicU64` and `SharedAtomicI64` mirror `std::sync::atomic`: `load`, `store`, `swap`, `compare_exchange`, the `fetch_*` family, and `fetch_update` for bounded counters.

```rust
use arion_wasm_sdk::shared::SharedAtomicU64;
use std::sync::atomic::Ordering;

let counter = SharedAtomicU64::try_new("requests_total")?;
let n = counter.fetch_add(1, Ordering::SeqCst) + 1;
```

**Versioned blobs.** `SharedBlob` holds an arbitrary byte buffer guarded by a monotonic version. `read()` returns the data with its version; `compare_and_swap(data, expected_version)` commits only if nobody wrote in between — retry on failure.

```rust
use arion_wasm_sdk::shared::SharedBlob;

let blob = SharedBlob::try_new("allowlist")?;
loop {
    let current = blob.read();
    let updated = revise(&current.data);
    if blob.compare_and_swap(&updated, current.version).is_ok() {
        break; // committed
    } // lost the race: re-read and retry
}
```

Plugins can also push telemetry from a hook — `set_custom_metrics` for key/value metrics and `set_access_log_operators` for fields in [access logs](@/docs/observe/access-logs.md).

## Callout or ext_proc?

A callout keeps the decision in your filter and asks a service one question per request; the filter still runs in-process and controls the outcome. [External processing](@/docs/extend/ext-proc.md) hands whole request and response phases to the service instead. Prefer a callout for small lookups feeding in-process logic; prefer ext_proc when the external service should own the transformation, is written in another language, or does work too heavy for the request path sandbox.

## Limits

- Callout targets must be clusters defined in your configuration; arbitrary URLs are not dialable.
- Shared variables live in proxy memory: they are scoped to one filter on one Arion instance, and reset on restart.
- Without `set_io_timeout`, a callout waits as long as the cluster takes; set a deadline for anything on the request path.
