NanoFaaS 0.23.0: the first release candidate

We just tagged NanoFaaS 0.23.0, and it is a milestone for us: the first version we are comfortable calling a release candidate. It took a month, 410 commits and 29 merged pull requests to get here. From now on, the plan is to stop piling up features and spend our time making the existing ones solid. The one exception is the peer-to-peer work, which is brand new and ships as beta.

Before the tour, a word on what “release candidate” means for us. It is a statement about scope, not about running mission-critical traffic. NanoFaaS is still a single control-plane instance, it keeps executions and queues in memory (only the function catalog is saved to disk), and it has no authentication. Keep it behind your own access control, and expect in-flight work to be lost if the control plane restarts.

The control-plane core with twelve optional modules in five groups. New in 0.23.0: containerd-deployment-provider, and p2p-discovery and forecasting, both in beta; offload gains a one-shot mode in beta.
The twelve optional modules around the control-plane core. Highlighted: what is new in 0.23.0.

Functions on rootless containerd

Until now you had two places to run functions: a Kubernetes cluster, or a single machine with a Docker-compatible runtime. 0.23.0 adds a third: plain containerd, running rootless with crun. If you want functions on one box without a Docker daemon, and without anything running as root, this is the backend for you.

It does what the other two do. It applies CPU and memory limits, waits for a replica to be ready before sending it traffic, scales up and down, and picks up running replicas again after the control plane restarts. You can also point it at registry mirrors, plain-HTTP registries or a custom CA. As with the other backends, you choose it when you build NanoFaaS, and you pick exactly one. The containerd guide walks through the setup.

Change the scheduler without restarting

NanoFaaS has two ways of queueing work: one queue per function, or a single shared queue. In 0.23.0 both run on the same engine, and you can switch between them on a live system. Callers waiting at the moment of the switch still get their answers. That makes it easy to try both under your real workload and keep the one that behaves better.

You choose the starting strategy with nanofaas.scheduler.strategy. To switch later, enable the runtime-config admin API and send a PATCH:

curl -fsS -X PATCH http://localhost:8080/v1/admin/runtime-config/scheduler \
  -H 'Content-Type: application/json' \
  -d '{"expectedRevision":0,"values":{"strategy":"shared-queue"}}'

Read the current revision with a GET first; a stale one gets a 409 back, so two people cannot overwrite each other’s change. This works when both queue modules are in the build, which is the default. And remember that the admin API has no authentication, which is why it is off unless you turn it on.

While we were in there, retries got politer: they now wait as long as Retry-After asks instead of firing again straight away. A function that has just been deployed also gets no traffic until it is actually ready.

Native builds from any laptop

Recipes are how you describe your own NanoFaaS distribution: which modules, which functions, which images. Version 2 of the format adds three things we kept missing.

schemaVersion: 2
name: demo
registry:
  repository: ghcr.io/my-org
  tag: "1.0.0"
  platforms: [linux/amd64, linux/arm64]
  provenance: true
controlPlane:
  modules: [async-queue, container-deployment-provider, build-metadata]
  build:
    mode: native
    builder: container
    native: {optimization: s}

Your version 1 recipes keep working unchanged. We also fixed a subtle mismatch: a native build now reads the recipe’s configuration, so the JVM and native versions of the same recipe end up with exactly the same components.

Rust, and lighter function images

You can now write functions in Rust. The SDK follows the same contract as the others, fn-init can scaffold a Rust project, and the four example functions come in Rust as well. That brings the list to Java, Python, Go, JavaScript and Rust, plus Bash examples.

We also put the function images on a diet. Smaller images mean faster pulls and quicker cold starts; the Python example is now ready in about 0.8 seconds instead of 1.

Language Before Now
Go ~21.5 MB ~8.9 MB, a static binary on scratch
Python (word-stats) 156.6 MB 72.8 MB on Alpine
JavaScript 165.5 MB (Node 20) 108.9 MB (Node 24)

In beta: nodes that cooperate

The newest part of 0.23.0 is aimed at setups with several NanoFaaS nodes, for example at the edge. All of it is off unless you enable it, and its configuration and APIs may still change, which is why it is labelled beta.

The less visible work

A release candidate has to earn the name, so a large share of this release went into things you will hopefully never notice. Our release checks now stop on any failed or skipped job and on any new static-analysis finding. We cleared all 538 open SonarQube issues across the five languages. We fixed several native-build problems and a control plane that refused to restart when a saved function had explicit resource limits, and timeouts from external functions are now reported as timeouts rather than generic errors.

Before you upgrade

Give it a spin

git clone --branch v0.23.0 https://github.com/Nanofaas/nanofaas.git
cd nanofaas
./gradlew :control-plane:bootRun

From there, the quick start has you registering and invoking a function in a couple of minutes. The full list of changes is in the diff since 0.22.0.

This is the version we want you to try and to break. If something is confusing, missing or just wrong, open an issue. Reports like that are what will turn this candidate into a release.