Guides

Serving components

Build a proof service on StateSync-GKR with the serving components and the reference server of version 1.2.0, choose a serving mode, and size admission, the return budget and the workers with the settings that were measured.

StateSync-GKR proves and verifies. A service around it also admits requests, frames replies, bounds its memory and keeps every core busy between requests. The ssgkr-serving crate in the repository's benches/serving folder packages those parts as a library and composes them into a reference proof server, together with a load frontend and real-TCP checks. It calls only the public statesync-gkr API, so its proofs are byte for byte the proofs of the library's own batch calls. The crate's README (opens in a new tab) is the source of this page.

The reference server is the server behind the v1.2.0 measurements in Serving capacity. This page explains its parts, its modes and the settings those measurements used.

Before you begin#

  • Read Production integration. It describes what a service adds around the engine and what it must record.
  • Work with prepared circuits, as described in Prepared execution. Every completion helper takes a PreparedSync.
  • Production use that generates proofs, embeds or redistributes the proving functionality, or offers proof generation to third parties requires a commercial license; see StateSync-GKR licensing. The crate is distributed under the same Business Source License 1.1 as the engine.

What the crate provides#

ModuleWhat it gives a service
wireThe request and reply envelope bound to a run identifier and a job identifier, length-prefixed framing with frame and metadata limits checked before work is admitted, and a reply writer that holds one absolute deadline for a whole frame
admissionAdmission, a cap on admitted jobs that have not started computing, shared by every connection, and RejectReason, the explicit refusals a client receives instead of unbounded queueing
budgetBudget, a return budget counted in responses and encoded bytes. A Permit stays charged through the frame write and its log row, so a stalled reader slows the producers instead of growing memory
completioncomplete, which builds the witness, proves, encodes, hashes and releases the typed intermediates of one request on one thread; complete_batch, the same per job inside a Rayon pool; and ContinuousRunner, a fixed set of workers that each start the oldest waiting job as soon as they finish the previous one
timingClocks, thread labels and StageTimes, the per-job record of dispatch, witness, proof, encoding, hash and release, including which thread released the job's memory
fixtureThe deterministic request constructor of the measured benchmark corpus

Each module works on its own, and the reference server in src/bin/ssgkr-serving composes all of them. Proof bytes do not depend on the mode, thread or order. complete calls make_job_prepared and prove_batch_prepared with a single job, the same per-job path as the library's sequential and parallel batch calls, and every transcript starts from its own job's public inputs. The server repeats this check at startup: each control sample must produce the same bytes through fused completion and through the batch API before the server listens.

The parts it covers#

A service built on the library alone had to write each of these parts itself. With the crate, most of them are components:

Part of a proof serviceWhere it lives
Replies matched to requestswire::Job and wire::Reply carry the run identifier and the job identifier; the frontend checks both against the request it sent
A bounded queue with explicit refusalsadmission::Admission and admission::RejectReason
Framing and size limitsLength-prefixed frames with a 2 MiB limit on JSON headers and a 16 MiB limit on proof bytes, run and kind identifiers bounded before admission, and wire::write_reply_deadline
Bounded memory for finished repliesbudget::Budget
Parallel witness generation and outputcompletion::complete_batch and completion::ContinuousRunner; each worker builds, proves, encodes, hashes and frees its own job
Per-request timingtiming::StageTimes, written by the reference server as job_stages

Your service still supplies authenticated roots, the state store and the order of updates (Trust boundaries), client authentication and transport security, process supervision and deployment. The crate records stage times and the reference server writes JSON-lines records; exporting metrics to your monitoring system is your code. ContinuousRunner runs its own threads, so an async service hands requests to it through a channel instead of proving on the executor.

Add the crate#

The crate is a standalone Cargo workspace inside the repository. It is not published to crates.io, and nothing in it changes the product workspace, the proof format, the verifier or the transcript order. Build and test it from a checkout:

Shell
cargo build --release --locked --manifest-path benches/serving/Cargo.toml
cargo test --release --locked --manifest-path benches/serving/Cargo.toml

To call the components from your own crate, depend on the folder in a checkout pinned to the commit you reviewed. The crate reaches the engine through a relative path in the same checkout, so depend on the engine through that checkout too; two copies of statesync-gkr from different sources would give two incompatible PreparedSync types.

Cargo.tomlTOML
[dependencies]
statesync-gkr = { path = "../statesync-gkr" }
ssgkr-serving = { path = "../statesync-gkr/benches/serving" }

Keep your project outside the checkout, as Installation explains. The crate's lock file uses the dependency versions of the engine's lock file.

A minimal continuous loop, as an excerpt (connection handling, logging and error replies omitted):

Rust
use ssgkr_serving::completion::{complete, ContinuousRunner};
use std::sync::{mpsc, Arc};

let (ingress, queue) = mpsc::sync_channel(8192);
let shared = Arc::new((prover, prepared));
let runner = ContinuousRunner::spawn(workers, queue, move |request| {
    let (prover, prepared) = &*shared;
    match complete(prover, prepared, request) {
        Ok(done) => { /* acquire a return permit, then hand done.bytes to the writer */ }
        Err(e) => { /* reply with an explicit error */ }
    }
})?;
// Readers call admission.try_admit() and ingress.try_send(request); dropping
// every sender lets the workers drain the queue and stop.

complete returns CompletionError and never a partial proof. A request of another operation kind than the preparation fails there, because make_job_prepared returns SyncError::KindMismatch in every build profile.

Choose a serving mode#

The reference server has four scheduling modes. Every mode shares the same admission, rejection, framing, return budget and shutdown behavior, and every mode produces the same proof bytes.

ModeSchedulingReply orderp99 at 1,000 requests/s, 192 cores
continuous (default)One arrival-order queue across kinds; up to --max-in-flight workers each start the oldest admitted job when they finish the previous one, and the computing worker hashes and frees its jobCompletion order, matched by job identifier33.7–33.9 ms
batch-fusedOne queue per operation kind, flushed at --batch-max or --max-wait-us; each job's witness, proof, encoding, hash and release run as one task on its worker inside the parallel stageBatch order95.6–96.0 ms
queued-returnAs in original, but per-connection writers hash and write while the next batch computesBatch orderNot part of the controlled runs
originalThe serving loop of the earlier capacity measurements: one compute batch at a time, each result hashed, written and logged before the next batchBatch order159.2–162.0 ms

The last column comes from three 60-second runs per mode of the same v1.2.0 binary on one 192-core VM, offered 1,000 requests per second from a separate 16-core frontend; v1.1.0, measured the same day with its own server, had p99 433.8–475.1 ms. Serving capacity has the conditions.

  • Use continuous when requests are independent and the client matches replies by job identifier. It had the lowest tail at every rate where both v1.2.0 modes met one second. Past the one-connection limit on 192 cores, batch-fused kept completing more requests per second, so size connections as described below.
  • Use batch-fused when a client expects replies in batch order.
  • Use queued-return or original only to compare against the earlier loop inside one binary.

Run the reference server#

Shell
cargo build --release --locked --manifest-path benches/serving/Cargo.toml
target=benches/serving/target/release/ssgkr-serving
"$target" server --out results/server-1 --listen 127.0.0.1:9400 --serving-mode continuous --workers 16 --max-in-flight 14
"$target" load --out results/load-1 --targets 127.0.0.1:9400 --rate 100 --duration-sec 30 --verify-workers 8

Build for the machine that runs the prover, as Installation explains; on the measured host the default release build proved 1.66–1.71 times slower than a native build. Every result directory must be new, because the server and the frontend refuse to overwrite files. The binary also has controls, which runs the startup controls on their own, and sweep-plan.

Size admission, the return budget and workers#

SettingOptionDefaultMeasured settingWhat it bounds
Compute threads--workers48Equal to the cores: 48, 96 or 192Parallel work; in continuous, the thread budget that bounds --max-in-flight
Jobs computing at once--max-in-flightWorkers minus 2, at least 1The default: 46, 94 and 190continuous only; memory for jobs in progress grows with it
Batch size and wait--batch-max, --max-wait-us96, 1,000 µsEqual to the cores, 5,000 µsBatch modes only
Admission cap--max-queue8,1928,192Admitted jobs that have not started computing, across connections
Return budget--return-jobs, --return-bytes192, 64 MiBThe defaultsFinished replies waiting to be written, and their encoded bytes
Connections--max-connections64One frontend connection; two in the two-connection runsConcurrent connections; extra connections are closed
Frame deadline--io-timeout-ms5,000The defaultOne whole reply frame; a missed deadline closes that connection
  • Start from one worker per core and the default number of jobs in flight. On 48 cores the measured limit was server CPU, so a smaller machine needs its own measurement.
  • Plan the reply traffic. Every reply carries one complete encoded proof of about 177 to 187 KB at depth 24, so 3,300 replies per second is about 4.8 Gbit/s. One connection stopped near 4.96 Gbit/s in the measurements, and in 15-second screens two connections to the same server carried 7.30 Gbit/s. Open more connections, or spread clients across servers, when one connection would carry more than that.
  • Size the return budget for the replies you let wait. It counts encoded buffer capacity only: jobs in progress, prepared circuits, request frames, thread stacks, socket buffers and allocator overhead are outside it, so measure whole-process memory as well. With the defaults, peak server memory at 5,100 requests per second on 192 cores was 535.2 MiB sampled from /proc (393.5 MiB by GNU time).
  • The admission cap decides when overload turns into a refusal. A job admitted beyond what the workers finish within your latency budget will be late, so a lower cap makes overload visible as refusals sooner. The measured runs kept the default of 8,192.

Rejections and overload#

An overloaded server answers with a refusal that the client can count, instead of queueing without limit. A rejected reply carries an empty proof and one of these reasons in its error field:

ReasonWhen it is sent
server_total_queue_fullThe admission cap was reached
server_ingress_queue_fullThe bounded hand-off to the scheduler was full
server_stoppingThe server is stopping and no longer schedules work
server_run_limit_before_computeThe run limit arrived before this admitted job started computing
encoded_response_exceeds_return_budgetThe encoded proof does not fit the frame or return budget limits
input:, witness: or encode: followed by a messageThe request could not be built, proved or encoded

When admission closes at the run limit or because the stop file appeared, admitted jobs that have not started receive server_run_limit_before_compute, and jobs already computing finish and are sent. terminal.json records which of the two closed the run.

Count every refusal as a failure in your success fractions, together with late and unanswered requests. In the overload runs on 192 cores, the frontend's own limit of 8,192 pending requests held back the excess before the server's admission cap was reached, so the server rejected no request, kept its memory at about a fifth of its limit and exited normally; Serving capacity has the numbers.

Records and stage times#

The server writes metadata.json, server.jsonl and terminal.json; the frontend writes metadata.json, frontend.jsonl, fixtures.jsonl and terminal.json. Reply rows add mode, connection and writer_tid to the fields of the earlier capacity caller, and the fused modes add job_stages:

job_stages fieldMeaning
worker_tid, worker_os_tidProcess thread label and, on Linux, the operating-system thread of the computing worker
dispatch_unix_nsJob start
witness_ns, prove_ns, encode_ns, hash_ns, release_nsStage durations on the worker
release_tidThread that freed the typed witness and proof
return_enqueue_nsFrom job completion to the start of its frame write
write_start_unix_ns, write_end_unix_nsFrame write interval

Use return_enqueue_ns to see replies waiting for the writer or the return budget, and prove_ns to watch the proving step alone. In continuous there are no batch rows; a reply's batch fields describe its own job. Reply headers have the same fields in every mode, so other load callers that speak this frame format accept them unchanged. Durations come from one clock on the worker; never subtract timestamps taken on different hosts.

Check a build#

Shell
cargo fmt --manifest-path benches/serving/Cargo.toml --check
cargo clippy --manifest-path benches/serving/Cargo.toml --all-targets --locked -- -D warnings
cargo test --manifest-path benches/serving/Cargo.toml --release --locked
python3 benches/serving/service-checks.py --binary benches/serving/target/release/ssgkr-serving --suite smoke --out results/checks-1

service-checks.py (opens in a new tab) runs real-TCP suites against the built server:

SuiteWhat must hold
smokeInvalid options fail before any output; in every mode a normal load is fully accepted, malformed inputs get explicit errors, and a flipped proof byte, a changed job identifier and a changed run identifier are each caught by the frontend
metadataRun and kind identifiers at the limit are served; one byte over closes the connection before admission
slowA slow reader causes real socket backpressure and return-budget waits but no failed frame; a reader that never reads fails at one frame deadline; a run limit during a burst rejects the jobs that had not started
ordercontinuous replies leave out of arrival order and every reply is still accepted
compareThe same load in every mode gives the same proof bytes
memoryWith a reader that stops reading, computation stops at the return budget and whole-process memory growth stays under an explicit bound

These checks run on one machine and confirm behavior; their latencies are not capacity evidence. The controlled measurements are in Serving capacity.

Next steps#