Rust Functions

Writing Rust functions with fission

Write, build, and deploy Rust functions on Fission, compiled by the builder into native server binaries for native-speed invocations with no per-request process startup.

The runtime is built on axum and tokio. This guide covers setting up the environment, writing functions, handling HTTP requests/responses, and working with dependencies.

Before you start

We’ll assume you have Fission and Kubernetes set up. If not, head over to the install guide. Verify your Fission setup with:

fission version

Add the Rust environment to your cluster

Rust is a compiled language, so source code must be compiled before it runs. The builder manager inside Fission does this automatically whenever a Rust function or package is created: the Rust builder converts a source package into a deployable native binary.

fission environment create --name rust \
  --image ghcr.io/fission/rust-env \
  --builder ghcr.io/fission/rust-builder

Rust environment image list

ImageBuilder Image
ghcr.io/fission/rust-envghcr.io/fission/rust-builder

Write a simple function in Rust

A single-file function is one .rs file that defines pub async fn handler. Any axum handler signature works, so you can use extractors, Json, headers, and so on.

Here is a hello world example (hello.rs):

use fission_rust::IntoResponse;

pub async fn handler() -> impl IntoResponse {
    "Hello, World!\n"
}

Create and test the function:

fission fn create --name helloworld --env rust --src hello.rs

Before accessing the function, make sure its package build has succeeded:

fission pkg info --name <pkg-name>

Now test it:

$ fission fn test --name helloworld
Hello, World!
See here for how to set up different triggers for a Rust function.

Single-file functions may use the crates pre-baked into the builder: fission-rust, axum, tokio, serde, and serde_json. For other dependencies, use a Cargo project.

HTTP requests and HTTP responses

The function receives every request routed to it; axum extractors give you typed access to all parts of the request.

Accessing HTTP Requests

Headers

Read a request header with an axum extractor:

use fission_rust::IntoResponse;
use fission_rust::axum::http::HeaderMap;

pub async fn handler(headers: HeaderMap) -> impl IntoResponse {
    let v = headers
        .get("x-my-header")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("not set");
    format!("x-my-header: {v}\n")
}

Create an HTTP trigger and call it:

fission httptrigger create --method GET --url "/<url>" --function <fn-name>
$ curl http://$FISSION_ROUTER/<url> -H 'X-My-Header: foo'
x-my-header: foo
Query string

Read query-string parameters into a map:

use std::collections::HashMap;
use fission_rust::IntoResponse;
use fission_rust::axum::extract::Query;

pub async fn handler(Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
    params.get("key-name").cloned().unwrap_or_default()
}
$ curl "http://$FISSION_ROUTER/<url>?key-name=123"
123
Request Body
Plain text

Read the raw request body as a string:

use fission_rust::IntoResponse;

pub async fn handler(body: String) -> impl IntoResponse {
    body
}
$ curl -X POST http://$FISSION_ROUTER/<url> -d foobar
foobar
JSON

Deserialize a JSON request body into a struct:

use fission_rust::IntoResponse;
use fission_rust::axum::Json;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
pub struct Msg {
    content: String,
}

pub async fn handler(Json(msg): Json<Msg>) -> impl IntoResponse {
    Json(msg)
}
$ curl -X POST http://$FISSION_ROUTER/<url> \
    -H 'Content-Type: application/json' -d '{"content": "foobar"}'
{"content":"foobar"}

Controlling HTTP Responses

Setting response headers and status codes

Return a tuple of status, headers, and body — axum converts it into a response:

use fission_rust::IntoResponse;
use fission_rust::axum::http::StatusCode;

pub async fn handler() -> impl IntoResponse {
    (
        StatusCode::CREATED,
        [("x-request-handled-by", "fission-rust")],
        "created\n",
    )
}
$ curl -i http://$FISSION_ROUTER/<url>
HTTP/1.1 201 Created
x-request-handled-by: fission-rust
...

Handler panics are caught by the runtime: the request returns HTTP 500 and the function keeps serving.

Multiple module files

A source archive may contain a handler.rs plus extra module files. Each file becomes a crate-root module, so handler.rs can reach a sibling util.rs as crate::util:

// util.rs
pub fn greeting() -> String {
    "Hello from a sibling module!\n".to_string()
}
// handler.rs
use fission_rust::IntoResponse;

pub async fn handler() -> impl IntoResponse {
    crate::util::greeting()
}
zip -r function.zip handler.rs util.rs
fission pkg create --name multimod --env rust --src function.zip

Working with dependencies (Cargo projects)

For third-party crates, supply a full Cargo binary crate as the source package. The only contract: the binary must serve HTTP on 127.0.0.1:$FISSION_RUNTIME_PORT.

The easiest way is the fission-rust SDK:

# Cargo.toml
[package]
name = "echo-example"
version = "0.1.0"
edition = "2024"

[dependencies]
fission-rust = { git = "https://github.com/fission/environments", rev = "<commit-sha>" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde_json = "1"

Pin the dependency to a commit with rev for reproducible builds. Without rev the git dependency tracks master and can change under you between builds; pinning a rev keeps every build resolving the same source. The builder pod fetches the crate over the network at build time, so the builder needs egress to GitHub — the same as for any crates.io dependency.

// src/main.rs
use fission_rust::IntoResponse;
use fission_rust::axum::Json;
use serde_json::Value;

async fn handler(body: String) -> impl IntoResponse {
    let value: Value = serde_json::from_str(&body)
        .unwrap_or_else(|_| Value::String(body));
    Json(value)
}

#[tokio::main]
async fn main() {
    fission_rust::serve(handler).await;
}

Archive and create the package as usual:

$ zip -r echo.zip Cargo.toml src
$ fission pkg create --name echo --env rust --src echo.zip
$ fission fn create --name echo --env rust --pkg echo

Bring your own framework

Because the contract is just “serve HTTP on $FISSION_RUNTIME_PORT”, any Rust web framework works — actix-web, rocket, warp, or raw hyper:

// src/main.rs — plain axum without the SDK
use axum::{Router, routing::get};

#[tokio::main]
async fn main() {
    let port: u16 = std::env::var("FISSION_RUNTIME_PORT")
        .ok().and_then(|p| p.parse().ok()).unwrap_or(8889);
    let app = Router::new().route("/", get(|| async { "Hello!\n" }));
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Multiple binaries and entrypoints

If the project defines several [[bin]] targets, the builder deploys all of them and the function’s --entrypoint selects one by name:

fission fn create --name myfn --env rust --pkg mypkg --entrypoint <binary-name>

A project with a single binary needs no entrypoint — it is always deployed under the default name handler.

How it works

The runtime image runs a small supervisor that implements the Fission environment interface. At specialization it starts your compiled function binary once and then reverse-proxies all requests to it over a pooled local connection, so steady-state overhead is a single localhost hop. If the function process exits, the pod is replaced automatically.

Modifying/Rebuilding the environment images

Refer to the Rust environment guide to learn about rebuilding the environment images.

Resource usage

Like any function, you can bound a Rust function’s resources:

fission fn create --name echo --env rust --pkg echo \
                  --mincpu 20 --maxcpu 100 --minmemory 64 --maxmemory 128

Compiled Rust functions are typically small (a hello world binary is around 1 MB) and have low memory floors, so they are a good fit for tight resource limits. Use kubectl top pod -l functionName=<name> while benchmarking to find the right values.