A minimal, modular Function-as-a-Service control plane.
NanoFaaS registers, deploys and invokes containerized functions on Kubernetes or on a single host. The control plane is one reactive Java service, and everything beyond invocation is a module you choose at build time.
- Java 25
- GraalVM native
- Kubernetes
- Docker
- containerd/crun
- MIT License
Latest updates
All posts →-
NanoFaaS 0.23.0: the first release candidate
Our first release candidate: functions on rootless containerd, schedulers you can swap on a running system, native builds from any laptop and a Rust SDK. P2P discovery arrives in beta.
Only what you need, nothing more
A build contains only the modules it needs. Queueing, autoscaling, concurrency control, hot reconfiguration, offloading and each deployment backend are opt-in.
-
Build-time modularity
Twelve optional modules with declared defaults, requirements and conflicts, validated before a single task runs.
-
One API, three execution modes
Managed deployments, passthrough to external endpoints and in-process execution share the same invocation endpoints.
-
Interchangeable backends
Kubernetes, a local Docker-compatible runtime, or rootless containerd/crun.
-
Concurrency control with SLOs
Per-function limits that are static, adapt to latency, or share a platform-wide budget with weighted max-min fairness.
-
Native builds
The control plane, CLI and Java example functions compile to GraalVM native executables; services ship on Distroless images.
-
Any language
Functions are HTTP services behind a small contract, with SDKs for Java, Python, Go, JavaScript and Rust.
-
Reproducible distributions
One versioned YAML recipe builds your own control plane and its functions, images included.
Quick start
You need Java 25; if it is missing, Gradle downloads a JDK. Clone the repository and open three terminals.
-
Start the control plane
./gradlew :control-plane:bootRun -
Start an example function on port 9090
./gradlew :functions:java:word-stats:bootRun --args='--server.port=9090' -
Build the CLI, register the function and invoke it
./gradlew :nanofaas-cli:installDist alias nanofaas="$PWD/clients/cli/build/install/nanofaas-cli/bin/nanofaas-cli" cat > word-stats.yaml <<'EOF' name: word-stats image: nanofaas/java-word-stats executionMode: EXTERNAL endpointUrl: http://localhost:9090/invoke EOF nanofaas fn apply -f word-stats.yaml nanofaas invoke word-stats -d '{"text": "to be or not to be", "topN": 2}'The response:
{"executionId":"cf9360a6-…","status":"success","output":{"averageWordLength":2.17,"wordCount":6,"topWords":[{"count":2,"word":"be"},{"count":2,"word":"to"}],"uniqueWords":4},…}
Asynchronous invocation
Enqueue a call and get an execution ID straight away.
nanofaas enqueue word-stats -d '{"text": "hello world"}'
nanofaas exec get <executionId>Plain HTTP
The CLI wraps the HTTP API, which you can also call directly.
curl -X POST http://localhost:8080/v1/functions/word-stats:invoke \
-H 'Content-Type: application/json' -d '{"input": {"text": "hello"}}'Next: run functions on Kubernetes, or write your own function.
Architecture
One control plane with a small core. Optional modules plug in around it, and a deployment provider runs the functions it manages.
A synchronous call (POST /v1/functions/{name}:invoke) is validated, rate-limited and checked for idempotency, then recorded in the execution store. It goes through sync-queue if that module is present, otherwise through async-queue, otherwise straight to the function. Metrics and health are served on port 8081.
| Execution mode | Behaviour |
|---|---|
DEPLOYMENT | The control plane owns the function's instances through a deployment backend. |
EXTERNAL | The function runs elsewhere; invocations are forwarded to its endpointUrl. |
LOCAL | In-process execution, for testing. |
Modules
Twelve modules around a small core, picked at build time. Each one declares its defaults, requirements and conflicts, and an invalid selection fails before any task runs.
Queueing and admission
Select both and they plug into one scheduler engine: pick the strategy at startup and switch it at runtime.
-
async-queueDefaultPer-function queues and a scheduler for asynchronous calls. Without it,
:enqueueanswers501 Not Implemented. -
sync-queueDefaultShared queue with admission control and backpressure for synchronous calls, rejecting work on saturation or excessive estimated wait.
Scaling and concurrency
-
autoscalerDefaultScales managed replicas from queue depth and in-flight work, within per-function bounds and cooldowns. Replicas still starting are never walked back.
Needs
async-queueorsync-queue -
concurrency-controlDefaultPer-function concurrency governor with five policies:
FIXED,STATIC_PER_POD,ADAPTIVE_PER_POD,BUDGETED(weighted max-min fair share of a platform budget) andSOJOURN.Needs
async-queueorsync-queue
Deployment backends
Pick one: the three providers are mutually exclusive. Without a provider, functions run in EXTERNAL or LOCAL mode.
-
k8s-deployment-providerDefaultRuns managed functions as Kubernetes deployments.
-
container-deployment-providerOptionalRuns functions as containers on a local Docker-compatible runtime.
-
containerd-deployment-providerOptionalRuns functions on rootless containerd with crun and CNI networking: no Docker daemon, no root.
Edge and multi-node
-
offloadDefaultTransparently proxies synchronous calls to a remote NanoFaaS, for example edge to cloud, either always or when the local
sync-queueis saturated. The execution stays tracked locally. -
p2p-discoveryOptionalNodes find each other over SWIM gossip, measure latency, choose active neighbours and exchange messages, optionally sharing functions, images and resource usage. Sized for tens to a few hundred nodes.
-
forecastingOptionalForecasts each function's external arrival rate, either with an EWMA over fixed time windows or from an uploaded trace.
Operations
-
runtime-configDefaultHot reconfiguration through a namespaced admin API. Other modules contribute the parameters they let you change.
-
build-metadataDefaultDiagnostics endpoint reporting how the running artifact was built, so you can prove which build is serving.
Choose modules
./gradlew :control-plane:bootJar -PcontrolPlaneModules=none
./gradlew :control-plane:bootJar -PcontrolPlaneModules=async-queue,autoscaler
./gradlew :control-plane:bootJar -PcontrolPlaneModules=allShip it as your own distribution
To fix the module selection together with build mode, configuration and functions, write a recipe.
Build your own NanoFaaS
A recipe is a versioned YAML file that describes a custom NanoFaaS distribution: which control plane to build, how, and which functions to package with it. One command turns it into artifacts and container images.
- Exact module set for the control plane, validated before anything is built.
- JVM or GraalVM native build, with per-component native-image options.
- Spring configuration baked into the control plane.
- Functions and services from any SDK, packaged as images next to it.
- Your registry and tag, with optional multi-architecture images.
Validation and preview need no Docker daemon, GraalVM or registry credentials, and the preview shows exactly what the build will use.
schemaVersion: 2
name: edge-demo
registry:
repository: ghcr.io/my-org
tag: "1.0.0"
platforms: [linux/amd64, linux/arm64]
controlPlane:
modules: [async-queue, container-deployment-provider, build-metadata]
build:
mode: native
functions:
- name: word-stats
sdk: java-lite
build: {mode: native}
container: {image: word-stats-java-lite}Four commands
./gradlew listRecipeFunctions # functions available to package
./gradlew validateRecipe -Precipe=recipes/edge-demo.yaml # validate and preview, builds nothing
./gradlew assembleRecipe -Precipe=recipes/edge-demo.yaml # build artifacts and images
./gradlew publishRecipe -Precipe=recipes/edge-demo.yaml # build, then push to your registryStart from recipes/local-demo.yaml or read the recipe guide.
Write functions in any language
A function is an HTTP service behind a small contract. SDKs make it a few lines; examples ship for every SDK language plus Bash.
- Java
- Python
- Go
- JavaScript
- Rust