Latency and Autoscaling Benchmark
SlimFaas includes a reproducible native-local benchmark for the warm synchronous proxy path, asynchronous enqueue and delivery, and scale-to-zero followed by PromQL scale-out.
Run the benchmark
The quick profile is intended for a smoke test and takes a few minutes:
BENCHMARK_PROFILE=quick .bin/slimfaas-local-benchmark.sh
Run the three-node recovery scenario
The restart-under-load scenario starts the same three-node SlimFaas Local
cluster, writes continuously to SlimData, generates application HTTP traffic,
kills a non-leader node, and fails unless the leader remains stable while the
restarted node reports both /health and /ready successfully and committed
Raft entries continue to advance:
RECOVERY_TIMEOUT_SECONDS=180 .bin/slimfaas-local-recovery-benchmark.sh
Use RECOVERY_SKIP_BUILD=true for repeated runs with existing Release binaries
and RECOVERY_RUN_ROOT to select the artifact directory. The scenario requires
lsof and writes recovery-summary.txt, traffic logs, and the local
supervisor log below the selected artifact directory.
The standard profile runs every case three times with longer measurement and warm-up windows:
.bin/slimfaas-local-benchmark.sh
The async-queue profile uses the same payload/concurrency matrix and adds a
low-rate paced scenario plus a burst of 1,000 messages at concurrency 64:
BENCHMARK_PROFILE=async-queue \
BENCHMARK_RUN_ROOT=artifacts/slimfaas-local-benchmark/async-baseline \
.bin/slimfaas-local-benchmark.sh
The script builds SlimFaas without the dashboard, builds the benchmark driver,
validates benchmarks/slimfaas.local.benchmark.yaml, starts a clean three-node
cluster with slimfaas local, runs the matrix, and shuts down every managed
process. Docker and Kubernetes are not required.
It requires the repository's .NET 10 SDK plus Bash and curl. Ports 31020-31023,
3162-3164, 31080, and the 32000-32015 process pool must be free.
Results are written below
artifacts/slimfaas-local-benchmark/<profile>-<UTC timestamp>/:
summary.md: tables for sync overhead, async latency, and scaling milestones;results.json: structured settings and results;latency-samples.csv: one row per measured HTTP request;scaling-timeline.csv: observed replicas and queue depths over time;benchmark-manifest.txt: commit, dirty-worktree flag, SDK, host, and matrix;slimfaas-local.log: control-plane and managed-process output.
Compare an optimization with its baseline
Run the standard profile before changing the runtime, then run the exact same matrix after the change. Compare the two structured reports with:
dotnet run --project src/SlimFaasBenchmark -- compare \
--baseline artifacts/slimfaas-local-benchmark/baseline/results.json \
--candidate artifacts/slimfaas-local-benchmark/candidate/results.json \
--output artifacts/slimfaas-local-benchmark/comparison
For an async queue optimization, capture the candidate on the same machine and compare with the async acceptance rules:
BENCHMARK_PROFILE=async-queue \
BENCHMARK_RUN_ROOT=artifacts/slimfaas-local-benchmark/async-candidate \
.bin/slimfaas-local-benchmark.sh
dotnet run --project src/SlimFaasBenchmark -- compare \
--profile async \
--baseline artifacts/slimfaas-local-benchmark/async-baseline/results.json \
--candidate artifacts/slimfaas-local-benchmark/async-candidate/results.json \
--output artifacts/slimfaas-local-benchmark/async-comparison
The command rejects reports whose duration, warm-up, repetitions, payloads, concurrency, or scaling settings differ. It writes:
comparison.md: human-readable before/after latency, overhead, throughput, async, scaling, and acceptance verdicts;comparison.json: the same comparison as structured data.
The command exits with a non-zero status when the candidate records errors or timeouts, when the median added p50 reduction is below 20% for 64 B–4 KiB or 40% for 256 KiB–2 MiB, or when added p95/p99 or the throughput ratio regresses by more than 10%. Async latency and scaling timings are reported for context; only their errors and timeouts affect the verdict.
With --profile async, the verdict additionally requires no missing,
duplicated, failed, or expired message; median reductions of at least 50% for
the HTTP-202 p95 and 40% for the function-arrival p95; and at least 40% p50
improvement for 2 MiB. Each case is also guarded against a p99 or throughput
regression above 10%. CPU per message may grow by at most 20%, peak memory by
10%, and Raft entries per message may not exceed 2x the baseline. Missing
resource fields in an older results.json remain readable and are displayed
as unavailable.
The synchronous and scaling sections are still generated by an async
comparison to expose collateral changes. Sync microbenchmark guardrails are
informational for --profile async; the async p99/throughput/resource rules and
all correctness failures determine that profile's verdict. Use the default
--profile sync comparison when sync guardrails must be gating.
What is measured
Warm synchronous overhead
The benchmark target is one minimal HTTP/1.1 server. The driver calls that exact process in two ways:
- directly at
http://127.0.0.1:31080/echo; - through
/function/benchmark-latency/echo.
For each payload size and concurrency, the report includes throughput and HTTP p50, p95, p99, and maximum latency. It then subtracts the direct percentile from the SlimFaas percentile in milliseconds and percent. Calling the same process on both paths isolates the warm routing/proxy overhead from application work.
The standard matrix uses:
- payloads: 64 B, 4 KiB, 256 KiB, and 2 MiB;
- closed-loop concurrency: 1 and 16;
- warm-up: 2 seconds;
- measurement: 10 seconds;
- repetitions: 3.
The report uses the median percentile across repetitions and the mean throughput. The driver alternates direct/proxy order between cases to reduce systematic CPU frequency and temperature bias.
Asynchronous latency
Each async case reports two different clocks:
- acceptance latency: client send until SlimFaas returns HTTP 202 after the request has been durably enqueued;
- arrival latency: client send until the target handler starts, including enqueue, queue wait, dispatch, and request-body transfer.
For both clocks, the report gives the mean (the simple average) and p50/p95/p99.
For example, p95 means that 95% of messages were no slower than that value. The
mean is the easiest headline number, while p95 and p99 reveal occasional slow
messages that an average can hide.
The target also records completion latency in results.json. The target and
driver are local processes on the same clock, so UTC ticks can be compared
without a network clock-synchronization error. The benchmark waits for every
accepted message to be observed by the target and for queue metrics to remain at
zero before starting another case.
The 2 MiB case is intentionally above SlimFaas' 1 MiB async body-offload threshold. This makes the matrix cover both inline queue payloads and the cluster-file path.
The async profile also records throughput, missing and duplicate IDs, errors, expirations, Raft entries per accepted message, process CPU per message, and peak aggregate SlimFaas working set. The paced scenario measures how quickly a nearly idle queue wakes without waiting for the polling fallback. The burst scenario measures coalescing and backpressure under sudden load.
Scaling speed
benchmark-scale starts at zero replicas. The driver sends a burst of 200 async
messages at concurrency 32; each target request takes 250 ms and each replica is
limited to one concurrent request. The following durations are measured from
the first client send:
- first and last HTTP 202;
- desired replicas changing from 0 to at least 1;
- first ready replica;
- desired replicas reaching 4;
- all 4 replicas becoming ready;
- the ready, in-flight, and retry queues returning to zero.
The first delivered /work request makes the benchmark target expose
slimfaas_benchmark_scale_pressure 4 for 30 seconds. SlimFaas scrapes it every
second and evaluates
max_over_time(slimfaas_benchmark_scale_pressure[30s]). This deterministic
demand signal exercises metric discovery, scraping, the internal metric store,
PromQL evaluation, scale policy application, local process launch, and health
readiness. Queue metrics are observed independently to measure backlog and drain
time.
Configure the matrix
The wrapper accepts environment variables without changing tracked files:
BENCHMARK_PROFILE=quick \
BENCHMARK_DURATION_SECONDS=5 \
BENCHMARK_WARMUP_SECONDS=1 \
BENCHMARK_REPETITIONS=2 \
BENCHMARK_PAYLOAD_BYTES=64,4096,1048576,2097152 \
BENCHMARK_CONCURRENCY=1,8,32 \
BENCHMARK_SCALE_MESSAGES=500 \
BENCHMARK_SCALE_CONCURRENCY=64 \
.bin/slimfaas-local-benchmark.sh
Available variables are:
| Variable | Standard default | Purpose |
|---|---|---|
BENCHMARK_PROFILE |
standard |
quick, standard, or async-queue defaults |
BENCHMARK_DURATION_SECONDS |
10 |
measured seconds per route and case |
BENCHMARK_WARMUP_SECONDS |
2 |
unrecorded warm-up seconds |
BENCHMARK_REPETITIONS |
3 |
repetitions per payload/concurrency case |
BENCHMARK_PAYLOAD_BYTES |
64,4096,262144,2097152 |
comma-separated body sizes |
BENCHMARK_CONCURRENCY |
1,16 |
comma-separated closed-loop concurrency |
BENCHMARK_SCALE_MESSAGES |
200 |
messages in the scaling burst |
BENCHMARK_SCALE_CONCURRENCY |
32 |
burst enqueue concurrency |
BENCHMARK_SCALE_TIMEOUT_SECONDS |
120 |
maximum scaling observation window |
BENCHMARK_ASYNC_DRAIN_TIMEOUT_SECONDS |
180 |
maximum async delivery/drain wait |
BENCHMARK_ASYNC_PACED_MESSAGES |
100 |
messages in the low-rate wake-up scenario |
BENCHMARK_ASYNC_PACED_INTERVAL_MS |
100 |
interval between paced messages |
BENCHMARK_ASYNC_BURST_MESSAGES |
1000 |
messages in the burst scenario |
BENCHMARK_ASYNC_BURST_CONCURRENCY |
64 |
burst enqueue concurrency |
BENCHMARK_RUN_ROOT |
timestamped artifacts path | exact output directory |
BENCHMARK_SKIP_BUILD |
false |
reuse existing Release binaries for repeated runs on the same checkout |
Interpret the result correctly
- Subtract percentiles in milliseconds first. A sub-millisecond direct baseline can make a small absolute delta look large as a percentage.
- Async HTTP 202 is enqueue latency, not execution latency. Use target delivery percentiles for end-to-end dispatch behavior.
- Native local scaling measures SlimFaas plus operating-system process startup. It deliberately excludes Kubernetes scheduling, admission, image pulls, CNI, and container runtime time. Run the same driver against a Kubernetes deployment when those costs are part of the question.
- Function status endpoints have short caches. Scaling times are externally observed milestones with that polling resolution, not internal timestamps.
- Avoid unrelated workloads, power-saving mode, thermal throttling, and debug builds. Keep the same host, SDK, commit, and matrix for before/after comparisons.
- A single run is exploratory. Use the standard repetitions, compare medians, and retain the raw manifest and samples for a publishable result.
See Native Local Development Mode for the process orchestrator and Autoscaling for the production scaling model.