§ Coverage
§ Introduction
What would a client-server communication engine look like built around Noise, io_uring, and SIMD? UCall keeps application handlers synchronous while its connection state machines overlap network I/O.
UCall is a ¹ performance-oriented, ² portable, and ³ compact client-server communication engine, with a ⁴ twist! How ¹ fast? About 291,000 1 KB echoes per second from one server thread natively, about 278,000 through its Rust bindings and about 268,000 through its Python bindings, against about 83,000 for gRPC on tonic, 21,000 for FastAPI and 15,000 for Python gRPC, in the framework comparison. How ² portable? A C ABI fronts a C++20 core with POSIX, Linux epoll with AIO and io_uring, macOS and FreeBSD kqueue, and Windows IOCP backends. IOCP runs one event loop per interface. Python and Rust bindings wrap that ABI directly, with the same caller-owned polling threads. How ³ compact? HTTP framing, routing, and transport state live in templated headers, with one native library exporting the C interface. What’s the ⁴ twist? For encrypted native connections, it uses the Noise secure communication protocol instead of implementing TLS.
Moreover, it’s easy to use synchronous handlers, even when the underlying backend uses asynchronous IO like Linux io_uring.
And it provides server-side batching for high-latency batch-processing endpoints, like GPU inference tasks and OLAP database queries.
The native examples demonstrate binary echo, conditional key-value writes, atomic ledger transfers, and batched inference through the C ABI.
§ FastAPI UX Comparison
Both columns serve the same typed endpoint; UCall’s Python bindings bind its parameters in C and answer on the polling thread.
| FastAPI | UCall |
|---|---|
| |
| |
§ Build From Source
A source build needs CMake 3.28 and a C++20 compiler, and pulls StringZilla, ForkUnion and SmashTable as submodules:
git clone --recursive https://github.com/unum-cloud/UCall.git && cd UCall
cmake -B build_release -D CMAKE_BUILD_TYPE=Release -D UCALL_BUILD_SHARED=ON
cmake --build build_release --parallel
cmake --install build_release --prefix ~/.localThe install holds the C headers,
libucall_static,libucall_shared, aucall.pc, and a CMake package namingucall::staticanducall::shared, which a C program links like any C library. It also holdsucall, a command-line client for one HTTP or Noise call, as inucall -d @body.json http://127.0.0.1:8080/echo.-Hadds a header line,-d @-reads the body from standard input,-iprints the status line and every response field before the body, and--server-keypins the server for anhttp+noise://URL.ucall keygen FILEwrites a fresh Noise private key and prints its public half, and the exit code is 0 for a 2xx answer and the status’s hundreds digit otherwise. LeavingUCALL_BUILD_SHAREDoff installs the archive without the shared library and itsucall.pc.
pip install . builds the Python bindings from the same checkout, and cargo build the Rust crate; the contributing guide lists the presets, tests and cross builds.
§ C Quickstart
One callback reads a request’s body and replies with it, and main initializes the server, registers the route, and polls its one loop on the calling thread.
The listing is examples/hello.c, which CI links against both installed libraries:
#include <stdio.h>
#include <ucall/ucall.h>
static void echo(ucall_received_http_request_t request, void* context) {
ucall_server_t server = *(ucall_server_t*)context;
ucall_outcome_t outcome = {0};
ucall_bytes_view_t body = {0};
ucall_server_request_body_t read = {.server = server, .outcome = &outcome, .request = request, .body = &body};
ucall_server_request_body(&read);
ucall_server_reply_content_t reply = {.server = server, .outcome = &outcome, .request = request, .body = body};
ucall_server_reply_content(&reply);
}
int main(void) {
ucall_network_interface_t listener = {
.address = {(ucall_byte_t const*)"127.0.0.1:8080", 14},
.max_concurrent_connections = 1024,
};
ucall_server_options_t options = {.interfaces = &listener, .interfaces_count = 1};
ucall_server_t server = NULL;
ucall_outcome_t outcome = {0};
ucall_server_init_t init = {.server = &server, .outcome = &outcome, .options = &options};
ucall_server_init(&init);
if (outcome.status != ucall_status_success_k) {
fprintf(stderr, "%s\n", outcome.message);
return 1;
}
ucall_server_add_http_endpoint_t route = {.server = server,
.outcome = &outcome,
.uri_template = {(ucall_byte_t const*)"/echo", 5},
.verb = ucall_verb_post_k,
.callback = echo,
.context = &server};
ucall_server_add_http_endpoint(&route);
ucall_server_take_calls_t poll = {.server = server, .outcome = &outcome};
ucall_server_take_calls(&poll);
ucall_server_free(server);
return outcome.status == ucall_status_success_k ? 0 : 1;
}export PKG_CONFIG_PATH=~/.local/lib/pkgconfig LD_LIBRARY_PATH=~/.local/lib
cc examples/hello.c $(pkg-config --cflags --libs ucall) -o hello && ./hello &
curl -X POST --data 'Hello, UCall!' http://127.0.0.1:8080/echo§ Rust Quickstart
The crate wraps the same ABI, with closures for handlers and threads the application owns for polling:
use std::time::Duration;
use ucall::*;
let interface = Interface::new("127.0.0.1:0").pipelined_requests(16);
let mut server = Server::new(&[interface])?;
server.post("/sum", |request: Request| {
let sum: u64 = request.body().iter().map(|&byte| u64::from(byte)).sum();
let _ = request.reply(HTTPStatus::OK, &[], sum.to_string().as_bytes());
})?;
let batching = Batching { min_size: 8, max_size: 64, max_latency: Duration::from_micros(500) };
let infer = server.batch("/infer", Verb::Post, batching, |batch: Batch| {
for request in batch {
let _ = request.reply(HTTPStatus::OK, &[], b"scored");
}
})?;
let address = server.address(0).unwrap().to_owned();
let mut client = Client::new(ClientOptions { connections: 4, ..Default::default() })?;
std::thread::scope(|scope| {
for event_loop in 0..server.event_loops() {
let server = &server;
scope.spawn(move || server.take_calls(event_loop));
}
scope.spawn(|| infer.take_batches());
let sum = client.post(format!("http://{address}/sum"), vec![1, 2, 3]);
let scored = client.post(format!("http://{address}/infer"), b"pixels".as_slice());
server.stop();
assert_eq!(sum?.body(), b"6");
assert_eq!(scored?.body(), b"scored");
Ok::<(), Error>(())
})?;The Rust guide covers streaming, files, Noise and metrics.
§ Performance
The native benchmark driver measures complete HTTP exchanges through the C ABI, including connection reuse, response validation, throughput, latency percentiles through p99.99, and client CPU time per request.
It runs closed-loop by default, or open-loop at a fixed arrival rate that charges each call its delay from the intended send time.
Each run records the selected backend, CPU kernel class, application polling threads, connection budget, and calls in flight.
On the server side, ucall_server_event_loop_statistics() counts each loop’s submissions, completions, kernel enters, and copied bytes, which the echo example prints on exit to price one request.
The framework comparison serves a 1 KB loopback echo and a typed sum through UCall natively and through its Rust and Python bindings, axum, actix-web, FastAPI and Python gRPC from the same cores.
The guide also measures pipelining depth, Noise against TLS 1.3 behind nginx, scaling across event loops, and 10,000 connections.
The C ABI page describes the implemented native surface; SDK and authorization plans below are labeled as such.
§ Design Principles
§ Asynchronous Engine for Synchronous Applications
Talking to remote services over a network can cost much more than the local computation that handles a request.
Moreover, the packet exchange between the application and the network card involves multiple context switches and kernel calls.
To mitigate these latencies, UCall uses several asynchronous IO mechanisms under the hood, like io_uring.
Now the question is, how to expose this asynchrony to the user code?
The UCall way is to keep the application code synchronous.
No async/await keywords or future/promise handles.
For lightweight operations, a handler stays an ordinary synchronous function.
For heavy operations, there is no secret pill, but there is a common “latency hiding” trick - batching.
Batch endpoints group independent requests by their configured size and latency budgets.
Endpoint callbacks run synchronously on the application polling caller; a deadline cannot preempt a callback.
Registering a batch endpoint returns its ucall_http_batch_endpoint_t handle.
Each batch endpoint has one queue fed by every event loop and one application thread polling that handle through ucall_server_take_batches(), so its callback never runs on an event loop.
With max_pipelined_requests on an interface and max_pipelined_calls on a client, one connection carries several requests at once, so a single client can fill a batch; replies still leave in request order.
§ Caller-Owned Polling
Select the backend and CPU class in the server or client configuration.
The polling threads must support the selected CPU class; serial is a conservative choice when their capabilities differ from the initialization thread’s.
UCall creates no threads.
Register server routes before polling. ucall_server_take_call() advances one loop with a bounded wait; a zero timeout performs one nonblocking progress round. ucall_server_take_calls() drives that loop continuously until shutdown.
Each loop has one application owner, and ucall_server_stop() can wake it from another thread.
The first stop drains: listeners close, idle connections close, and every request already held gets its reply, announced as its connection’s last.
A second stop cuts the replies still in flight; there is no drain timeout.
An interface takes a listen_descriptor in place of an address, accepting on a socket systemd or a supervisor passed in, TCP or AF_UNIX, and never closing it.
Once an interface holds its whole connection budget, a new peer evicts the idle keep-alive connection that progressed least recently, and slow peers holding requests are never cut by a deadline.
ucall_client_call() submits an exchange and returns its acceptance result.
Each accepted call produces one final record through ucall_client_take_completed(), which drives I/O and retrieves completion batches.
Initialize, submit, poll, and free a client on the same owner thread.
Keep borrowed request inputs alive until their completion is retrieved.
After retrieval, the caller owns each response until ucall_client_response_free(), even past client teardown.
A response is capped by the client’s max_response_bytes, which takes the kernel’s largest receive buffer by default, as max_request_bytes caps a server’s requests.
Stop and join server polling threads before freeing the server.
The client resolves host names itself, through a stub resolver its own event loop drives, so a lookup never blocks the polling thread.
It reads /etc/resolv.conf with its search and ndots options and /etc/hosts on POSIX, and the adapters’ settings on Windows.
Queries for AAAA and A go over UDP and retry over TCP when truncated, follow CNAME chains, and cache answers and refusals for their TTL.
Connections race the answered addresses Happy Eyeballs style, alternating IPv6 and IPv4 from IPv6, waiting 50 ms for an AAAA answer behind an A one, and starting the next address 250 ms after the previous one.
The client’s log sink reports dials that fail every address, refused handshakes and refused redirects, in the same human or JSON lines the server writes.
§ Encryption: From TLS to Noise
There is a good reason to implement TLS in a networking library - it’s the most widespread encryption standard on the Internet. There is also every reason not to - TLS is a massive and complex standard, with multiple versions, dozens of cipher suites, countless configuration options, error-prone details, and poor ergonomics. Integrating TLS properly even using OpenSSL, BoringSSL, LibreSSL, and mbedTLS in a clean & performant way in line with principles of Unum - failed twice over two years.
Moreover, the certificate management, PKI, and trust chains are an absolute ergonomic nightmare. There is a better way… that is broadly deployed, but little known. The Noise Protocol Framework!
UCall uses a protocol+transport URL scheme convention (like Docker’s http+unix://) to explicitly specify the transport: http:// or http+noise://.
Noise uses a Diffie-Hellman (DH) key exchange to establish a shared secret between the client and server, with the handshake pattern determining which keys each side must know.
Several handshake patterns are defined, each suitable for different use cases:
- XX: Full mutual authentication where both parties authenticate each other during the handshake. Best for peer-to-peer scenarios or when the server doesn’t have a fixed identity known in advance.
- IK: Client knows the server’s public key in advance (Identity Known). Provides faster handshakes and is ideal for typical client-server architectures, similar to TLS with certificate pinning.
- KK: Both parties know each other’s public keys beforehand (Known Known).
A server interface names the one initiator key it admits through
noise_peer_key, and a client names the server’s key per call.
ucall_noise_key_generate() draws static keys from the operating system’s entropy, and ucall_noise_key_derive() recovers the public key that peers see from a stored private one.
Noise can be implemented in several ways, depending on the choice of three cryptographic primitives: Diffie-Hellman key exchange, symmetric cipher, and hash function. Examples of primitive combinations are:
| Name | Key exchange | Cipher | Hash |
|---|---|---|---|
25519_AESGCM_SHA256 | Curve25519 | AES-256-GCM | SHA-256 |
25519_ChaChaPoly_SHA256 | Curve25519 | ChaCha20-Poly1305 | SHA-256 |
25519_ChaChaPoly_BLAKE2s | Curve25519 | ChaCha20-Poly1305 | BLAKE2s |
448_ChaChaPoly_SHA512 | Curve448 | ChaCha20-Poly1305 | SHA-512 |
UCall implements exclusively the 25519_AESGCM_SHA256.
First, it uses the Curve25519 elliptic curve for the DH asymmetric key exchange to produce a shared secret.
Then, it uses the AES-256-GCM authenticated symmetric cipher for encrypting and decrypting the data.
Finally, it uses the SHA-256 hash function for hashing operations during the handshake and key derivation.
Why choose AES-256-GCM and SHA-256? Both primitives ship in StringZilla, so the transport shares the engine’s capability-selected kernels rather than maintaining a separate cryptographic backend. The selected CPU class carries matching string searches, AES-GCM, SHA-256, and atomic operations together. The implemented x86 and Arm kernels use the available cryptographic instruction paths, with serial fallbacks for the baseline class.
Noise itself sits beneath HTTP, independently of the socket backend. Payloads split into independently authenticated records, invisible to the protocol above. The record bound comes from its two-byte length prefix and authentication overhead; it does not become an application message-size limit.
§ Authentication: Macaroons, Biscuits, and Other Pastries
Roadmap: Native Macaroon and Biscuit parsers are not implemented. The following describes the authorization model being considered; current applications inspect headers and apply their own policy.
Traditional web authentication often combines OAuth2 flows, JWTs, and session cookies. Delegating access to part of a service can require additional token issuance, scopes, and consent flows; capability tokens offer another way to express those restrictions.
There is a better way, pioneered by Google in 2014: Macaroons. A Macaroon is a bearer token built on chained HMACs that supports attenuation - anyone holding a token can add restrictions to it, but nobody can remove them. No database lookup required for verification, just the server’s secret key.
Original token: user=alice, account=12345
↓
Alice restricts: user=alice, account=12345, time < 2025-01-01, method = GET
↓
Alice gives to Bob - Bob can only read, only until January
↓
Bob restricts: user=alice, account=12345, time < 2025-01-01, method = GET, path = /api/reports/*
↓
Bob gives to Carol - Carol can only read reports, nothing elseThe cryptography (HMAC chain) guarantees that restrictions can never be removed - only added. The server verifies the signature, then evaluates each restriction against the current request. No round-trip to a database, no OAuth dance for delegation.
Biscuit is the modern successor, replacing free-form restriction strings with a proper Datalog query language and swapping HMAC (symmetric) for Ed25519 (asymmetric). This means verification requires only the server’s public key - multiple services can verify tokens without sharing secrets.
// Biscuit token with Datalog restrictions
user("alice");
role("viewer");
check if time($t), $t < 2025-01-01T00:00:00Z;
check if operation($op), ["read", "list"].contains($op);
check if resource($r), $r.starts_with("/api/");The proposed integration would cover time bounds, path patterns, HTTP methods, IP ranges, and application-defined restrictions.
A possible Authorization header format is Authorization: Macaroon <base64> or Authorization: Biscuit <base64>; the current engine does not interpret either token.
Why “pastries”? Because capability-based tokens are delicious - they’re self-contained, offline-verifiable, and delegation-friendly. Macaroons are used by the Lightning Network for Bitcoin payments. Biscuits power Clever Cloud’s infrastructure. Both are battle-tested at scale.
§ Protocol Identity: SPIFFE
Roadmap: SPIFFE integration and SVID binding to Noise keys are not implemented.
Authentication tokens answer “what can this request do?” but not “who is making it?” In traditional deployments, workloads authenticate using static credentials - API keys, service account tokens, or certificates manually provisioned by operators. These secrets end up in config files, environment variables, and CI/CD pipelines, creating a sprawling attack surface.
SPIFFE (Secure Production Identity Framework for Everyone) takes a different approach: workloads prove their identity through attestation, not secrets. Instead of “here’s my password”, it’s “here’s cryptographic proof of what I am” - verified by inspecting process attributes, Kubernetes pod labels, AWS instance metadata, or other environmental signals.
A SPIFFE identity is a URI: spiffe://example.com/service/api-server.
The workload receives a short-lived identity document (SVID - SPIFFE Verifiable Identity Document), with its issuer and deployment policy controlling renewal.
No secrets in config files.
No manual certificate management.
If the workload dies and restarts, it re-attests and gets fresh credentials.
┌─────────────────────────────────────────────────────────────────┐
│ Traditional: Static secrets in config │
│ API_KEY=sk_live_abc123... ← Leaked? Game over. │
├─────────────────────────────────────────────────────────────────┤
│ SPIFFE: Identity from attestation │
│ Process starts → Agent inspects (PID, UID, k8s labels) │
│ → Issues a short-lived identity document │
│ → Renews before expiry │
│ No application API keys in config; workload attestation │
│ provides the basis for issuing its identity. │
└─────────────────────────────────────────────────────────────────┘A future integration would need to verify a binding between the SPIFFE identity and the Noise static key.
After an authenticated exchange, an application could authorize based on the client’s SPIFFE ID pattern: spiffe://prod.example.com/service/* allows any production service, while spiffe://prod.example.com/service/payment-* restricts to payment services only.
The intended separation is Noise for channel encryption, SPIFFE for workload identity, and Macaroons or Biscuits for delegated capabilities. Only the Noise transport and application peer-authorization callbacks are implemented today.
§ Engineered for Performance
The shared connection automata consume completions and emit I/O requests for POSIX, Linux epoll with AIO, Linux io_uring 6.12 or 6.18, kqueue, and IOCP.
HTTP framing, routing, and Noise stay in templated headers under include/ucall/.
One library exposes the C ABI once; server and client configuration select the backend and a compiled CPU class.
Per-CPU instantiation units specialize every enabled native engine, while ordinary C calls use their handle’s fixed operation table.
The implementation uses:
- StringZilla for SIMD searches, header normalization, SHA-256, and AES-256-GCM.
- ForkUnion for capability checks and extended atomic references.
- SmashTable for fallible containers and the examples’ strictly serializable transactions.
There is no OpenSSL, no libsodium, and no third-party Noise implementation. The Curve25519 implementation lives separately from StringZilla’s symmetric cipher and hash kernels.
§ REST Interface
The C ABI exposes path, query, header, and body views for REST-style APIs:
- Path Parameters: Captured by URI templates, e.g.,
/users/{user_id}/posts/{post_id}, throughucall_server_request_path_parameter() - Query Parameters: Looked up by name, e.g.,
/search?q=rust&limit=10, throughucall_server_request_query_parameter() - JSON Body: Applications parse raw body bytes, e.g.,
{"temperature": 0.7, "max_tokens": 100}, and the Python bindings bind typed handler parameters from it - Multipart Body: Files and metadata in one request, read part by part through
ucall_server_request_part() - Header Fields: Looked up by case-insensitive name through
ucall_server_request_header(), or walked in arrival order throughucall_server_request_fields_count()anducall_server_request_field(), as a proxy forwards a head whole - Raw Query: The whole query after the first
?throughucall_server_request_query() - Peer Address: The IPv4, IPv6 or AF_UNIX address a connection came from, beside its interface and authenticated Noise key, through
ucall_server_request_peer()
Paths and query values arrive as sent, and ucall_decode_percent() decodes their %XX escapes, reading + as a space in a query and refusing a % without two hexadecimal digits.
Field names arrive lowercased, as the parser keeps them, and values trimmed; a client response’s fields read the same way through ucall_client_response_fields_count() and ucall_client_response_field().
Routing is exact, as in axum and Envoy: an empty segment is a segment, so /users, /users/ and //users are three different paths.
Literal segments match before parameters, which never take an empty segment, and both before a trailing wildcard.
A path no route takes answers 404, and a path whose routes take other verbs answers 405 with an allow header listing them.
A GET route serves HEAD as well, an OPTIONS request answers 204 with the same allow list, and OPTIONS * lists every verb.
A request sending Expect: 100-continue gets its interim 100 Continue once the requests before it on its connection were answered, or on a streamed endpoint once its handler first pulls the body.
§ Multipart Data & Rich Types
Roadmap: Python rich-type conversion and the following SDK example await implementation or migration.
A multipart/form-data body lets an application send files and metadata in a single HTTP request.
The client supplies the boundary parameter, and ucall_server_request_parts_count() and ucall_server_request_part() split the buffered body on it the first time a handler asks.
Each part keeps its own Content-Disposition name and Content-Type, and its payload is a view into the receive buffer, never copied, so an image or audio decoder reads it in place.
The intended Python SDK could decode numpy.ndarray and PIL.Image inputs and outputs.
The following is an illustrative design sketch, requiring future SDK support and application-provided model and index setup, for a multi-modal semantic search platform built on top of Unum’s UForm models and USearch engine.
from ucall import Server
from usearch import Index
from uform import get_model
server = Server()
model = get_model('unum-cloud/uform-vl-multilingual')
@server
def vectorize(description: str, photo: PIL.Image.Image) -> numpy.ndarray:
image = model.preprocess_image(photo)
tokens = model.preprocess_text(description)
joint_embedding = model.encode_multimodal(image=image, text=tokens)
return joint_embedding.cpu().detach().numpy()
@server
def search(vector: numpy.ndarray, top_k: int = 10):
matches = index.search(vector, top_k)
return matches.keys.tolist()The intended wire interface could be called with standard tools like curl; this route is part of the sketch above:
curl -X POST http://localhost:8545/vectorize \
-F "description=A cat in a hat" \
-F "photo=@image.jpg"§ Client-Side Batching ≠ Server-Side Batching
When building high-throughput applications, it’s common to batch multiple requests together on the client side to reduce network overhead. That batch is one request from one client, which the server must split, dispatch and reassemble itself. Server-side batching instead collects independent HTTP requests from different clients and every polling loop into each batch endpoint’s queue, and passes their retained handles as a counted array. Each request keeps its own response and transaction boundary, as the key-value and ledger examples demonstrate.
§ Resumable Uploads & Downloads
Roadmap: Resumable-transfer policies and streaming multipart access are not implemented.
There is no one unique way to deal with resumable or chunked uploads and downloads. The most common approaches for uploads and downloads are sadly not symmetric.
| Upload | Download | |
|---|---|---|
| Standard | Tus.io | HTTP range requests (RFC 7233) |
| State | Stateful (server tracks progress) | Stateless (client decides ranges) |
| Headers | Upload-Offset, Tus-Resumable | Range, Accept-Ranges, Content-Range |
| Methods | POST, PATCH, HEAD | GET with Range header |
| Usage | Vimeo | Browsers and download clients |
Streaming multipart is another upload design, but continuation after a failure requires an application-level protocol rather than multipart framing alone.
Replies can carry byte ranges of files, as descriptors or as paths beneath directories an interface registers, which is what a Range response needs.
The event loop reads them with O_DIRECT, past the page cache, into windows borrowed from its pool, reading the next window while the previous one sends.
Every reply leaves through zero-copy sends wherever the kernel offers them, and a window returns to the pool once the kernel let go of it.
Under Noise each read leaves a gap behind every record’s payload for its tag and the next record’s header, so records seal in place inside the window.
IOCP does not yet send file parts over Noise connections.
§ Other Functional HTTP Headers
The C ABI exposes request headers and accepts application-supplied response headers. The following are useful application-level conventions; they do not imply built-in rate limiting, caching, tracing, or security policies.
Rate Limiting & Flow Control (Server → Client):
X-RateLimit-Limit: Maximum requests allowed per time windowX-RateLimit-Remaining: Remaining requests before hitting the limitX-RateLimit-Reset: Unix timestamp when the rate limit counter resetsRetry-After: Seconds to wait before retrying (with 429/503 status codes)X-Backoff-Ms: Custom millisecond-precision backoff hintX-Queue-Depth: Current server queue depth for client-side load awareness
Request Correlation & Tracing (Bidirectional):
X-Request-ID/X-Correlation-ID: Unique identifier linking logs and metrics across servicesX-Trace-ID/X-Span-ID: Application-defined distributed tracing identifiers
Caching & Validation:
ETag(Server → Client): Resource fingerprint for cache validationIf-None-Match(Client → Server): Conditional request using ETag, returns 304 if unchangedLast-Modified(Server → Client): Resource modification timestampIf-Modified-Since(Client → Server): Conditional request using timestampCache-Control: Caching directives (max-age,no-cache,no-store,private,public)Vary: Indicates which request headers affect response caching
Performance & Observability (Server → Client):
X-Response-Time: Server-side processing time in millisecondsServer: Server software identification (e.g.,UCall/1.0)
Security (Server → Client):
X-Content-Type-Options: nosniff: Prevents MIME-type sniffing attacksX-Frame-Options: Clickjacking protection (DENY,SAMEORIGIN)Content-Security-Policy: Controls resource loading to prevent XSS attacks
§ Streaming
Streamed replies are implemented: ucall_server_reply_begin(), ucall_server_reply_append() and ucall_server_reply_end() send a chunked body from any thread, long after the callback returned.
Streamed request bodies are implemented too: an endpoint registered with ucall_body_delivery_streamed_k runs once the head parsed, and each ucall_server_request_body_chunk() takes every byte decoded since the last one, finishing later through its outcome when none arrived yet.
The Python and Rust bindings expose both through a detached request a worker finishes, described in their Files & Streams sections.
The connection reads on only as the handler pulls, so a slow handler slows its own peer and nobody else, and no request pipelined behind the body parses before the body ended.
Buffered requests are capped by max_request_bytes, which defaults to the largest receive buffer the kernel grants a socket; past it the reply is 413.
§ Observability & Metrics
Every event loop sends StatsD/DogStatsD datagrams to an explicitly configured endpoint, on a connected UDP socket of its own driven by the loop’s I/O backend.
Metrics are opt-in: metrics_address names an endpoint such as 127.0.0.1:8125, and an empty view sends nothing.
A server should never send packets to a port its operator didn’t ask for.
Each metrics_interval_microseconds, one second by default, a loop reports the deltas of its counters since its last delivered datagram, its live connections per interface, and its refusals by status.
Loop zero also reports every batch endpoint’s queue and counters.
Request latencies, sampled at metrics_sample_rate, ride the same datagrams, which never pass the path MTU:
myapp.requests:1830|c|#env:prod,region:us-east,loop:0
myapp.connections:127|g|#env:prod,region:us-east,loop:0,interface:0
myapp.refused:4|c|#env:prod,region:us-east,loop:0,interface:0,status:413
myapp.latency:0.045|ms|@0.1|#env:prod,region:us-east,endpoint:/sum,method:POST,status:200
myapp.batch.queue:8|g|#env:prod,region:us-east,endpoint:/inferucall_bytes_view_t const tags[] = {{(ucall_byte_t const*)"env:prod", 8}, {(ucall_byte_t const*)"region:us-east", 14}};
ucall_server_options_t config = {
.metrics_address = {(ucall_byte_t const*)"127.0.0.1:8125", 14},
.metrics_sample_rate = 0.1, // A latency line for one request in ten
.metrics_prefix = {(ucall_byte_t const*)"myapp", 5}, // Metric names become "myapp.requests"
.metrics_tags = tags, // Global tags for every line
.metrics_tags_count = 2,
};A datagram that fails to send is dropped, and the next one carries its counters, so collectors see every request counted once.
§ Differences & Limitations
§ Where Would You Prefer WebRTC?
WebRTC DataChannels offer message-oriented communication with configurable reliability. That is a different tradeoff from UCall’s TCP byte streams.
TCP’s head-of-line blocking matters on lossy networks. In TCP, if a packet is lost, all subsequent packets are held back until the lost packet is retransmitted and received. On reliable networks, there should be no noticeable difference. In the future, WebRTC DataChannels may be added.
§ What About HTTP/2?
UCall currently supports HTTP/1.1. HTTP/2 adds several important features:
- Extended
CONNECTfor protocols that tunnel bidirectional traffic. - Multiplexing multiple streams over a single TCP connection to improve resource utilization.
- Header compression using
HPACKto reduce bandwidth usage for common and repetitive headers.
Those are all useful features.
Still, maintaining dynamic decompression tables for HPACK isn’t trivial.
Moreover, the CONNECT has its own nested state machine inside each stream.
Many such streams can be active simultaneously within the same TCP connection.
All of that increases the attack surface and the potential for bugs and didn’t align with UCall’s design goals.
§ Why not HTTP/3 and QUIC?
UCall currently only operates on top of TCP. Building around UDP has been a long-standing ambition for Unum since inception in 2015, and a QUIC-like protocol with Noise encryption makes a lot of sense, but it’s far less compatible with existing infrastructure. So for now, UCall focuses on TCP-based communication and HTTP/1.1 interoperability.
§ What Will You Miss From FastAPI?
FastAPI comes with a rich ecosystem of plugins and extensions for authentication, database integration, OpenAPI schema generation, and more.
UCall supports routes like /users/{id} natively, including named parameters and greedy suffixes, matched exactly as the REST Interface section describes.
The current gaps are higher-level SDK ergonomics, automatic schema generation, rich-type conversion, and the authentication integrations discussed above.
§ What If You Love TLS?!
If you really need TLS, you can always deploy UCall behind a reverse proxy, like Nginx or Envoy. They will handle all the TLS termination and forward decrypted HTTP traffic to UCall over a local network interface. This is a common pattern in production deployments, as it allows to offload TLS processing from the application server. The following deployment template assumes your application image binds plaintext HTTP on port 8080; replace the image and certificate paths for your deployment:
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- ucall
ucall:
image: your-ucall-application:latest
expose:
- "8080"And the corresponding nginx.conf:
upstream ucall_backend {
server ucall:8080;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://ucall_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}§ Citation
If UCall helps your research or product, please cite it:
@software{Vardanian_UCall,
author = {Vardanian, Ash},
title = {{UCall: Low-Latency Client-Server Networking Stack Built around Noise Encryption, io\_uring and SIMD}},
doi = {10.5281/zenodo.7951496},
url = {https://github.com/unum-cloud/UCall},
license = {Apache-2.0}
}§ License
Licensed under the Apache License, Version 2.0.
See LICENSE for details.