Rust Functions

Writing Rust functions with fission

Fission supports Rust as a first-class function language. Rust functions are compiled by the builder into native server binaries built on axum and tokio, so invocations run at native speed with no per-request process startup.

In this usage guide we’ll cover how to use this environment, write functions, and work 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
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
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
use fission_rust::IntoResponse;

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