API first service framework.

Write a service once and it speaks GraphQL from wherever it runs: a Linux server, a container, a Cloudflare Worker, a microcontroller on the bench over Bluetooth, USB and Serial Line. One schema from the code, one set of resolvers, the same API on every carrier.

The vision

The same code,
everywhere.

Aosabi carries on the vision of snek-functions. The logic of a service and its GraphQL API are the same code, so they never become inconsistent. The schema is generated from the types in the code, and the author writes service logic, never a schema definition. Types, interfaces and unions are all supported, and authentication and authorization are built in with minimal configuration.

A service built with Aosabi runs everywhere: on a server, serverless, and at the edge, and for Aosabi the edge reaches down to a device or a microcontroller such as an ESP32. That is why its binaries are as tiny, as fast and as secure as possible, with no unnecessary bloat, and why optimization matters in every part of it. Aosabi does one job and does it very well: being a GraphQL service.

A service is written in plain Rust or in TypeScript, and the build derives its GraphQL schema from that code. Aosabi stays a lean service framework, and its GraphiQL, with aoviewer, its schema viewer, inside, runs as a page at aosabi.org/dev.

The principle

The carrier changes,
the API does not.

Aosabi is a very small, very fast GraphQL server, and it is the server, nothing else. A carrier is the link a client reaches a service over. A protocol is named with its transport. GraphQL is the one invariant, and it needs no HTTP. Aosabi generates no client.

Status: M0 to M8, M10, PASS1 and TS1 done, no service deployed

The server answers, and that is what makes it the server. The client opens the connection.

GraphTP carries the GraphQL request a client sends over HTTP, query text included. Every build is designed to answer it at runtime, introspection included, with the HTTP status and the body.

The schema comes from the code, in Rust or in TypeScript, and the build writes it. One source is designed to build for every environment.

The author picks a card and nothing else. Five cards ship built into the command line, and the build resolves target, carriers and packaging by itself from the one it is given.

Elsewhere a socket carries bespoke framing, USB a vendor protocol and Bluetooth a packed struct, and every product designs all three again. Aosabi is a schema from code, API first framework, and the API is GraphQL, the same API on every carrier.
The six carriers and the protocols they carry
CarrierProtocol
NetworkHTTP(TCP) and GraphTP(Homa)
Unix SocketHTTP(Unix Socket) and GraphTP(Homa)
USBGraphTP(Homa)
BluetoothGraphTP(Homa)
Serial LineGraphTP(Homa)
LoRaGraphTP(Homa)
GraphTP, the Graph Transfer Protocol, is the protocol Aosabi speaks over Homa, specified apart from Aosabi so that any GraphQL server and client can implement it, and GraphTP(Homa) names it with the Homa transport protocol under it. It carries its JSON and its compression itself, with a head of 3 bytes on a request and of 2 on an answer, the HTTP status and a substatus. Aosabi is designed to implement the full Homa protocol itself, in its own crate homa, one engine in Rust for all six carriers, which also builds as a Linux kernel module. On Network Homa runs as its own transport protocol on IP protocol 146, on the crate homa over raw IP sockets, and a host where raw sockets are not allowed serves Network with HTTP(TCP). AoFrame is the framing of Aosabi. On a plain byte stream, such as Serial Line, USB serial or Unix Socket as a stream socket, or on the L2CAP LE credit based channel of Bluetooth, which some clients read as a byte stream, it marks where each Homa packet starts and ends.

The layers

Two peers,
one real wire.

The figure reads like any layered protocol, and its rows carry the layer numbers of the OSI model. Each layer talks with its peer on the other side, drawn as a dashed line, and reaches it through the layers below: it hands its data down, the bytes cross one real wire at the bottom, and they climb back up on the other side. The pink dots are that real path. Choosing a carrier in the drawing makes it the live carrier of the connection, and on Network and Unix Socket a second choice picks the protocol, on Bluetooth the path.

Two peer stacks and the wire between themRequestResponseScrolls sideways Select a carrier
CLIENTNot AosabiSERVERAosabiPeer talkReal pathWRITTENONCEPER PROTOCOLPER CARRIERTHE SEAML7ApplicationExecutorexecute(query, vars)L7App ProtocolL7App ProtocolL6PresentationL6PresentationL5SessionL5SessionL4TransportL4TransportL3PackagingL3PackagingL2FramingL2FramingL1CarrierL1CarrierOperationSchemaResolversGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaHTTPJSONEdge codedzstd,br,gzipMessageboundariesRequest andresponse pairingTLSTCPHTTPJSONEdge codedzstd,br,gzipMessageboundariesRequest andresponse pairingTLSTCPHTTPJSONDeflate≥1,024 BMessageboundariesRequest andresponse pairingHTTPJSONDeflate≥1,024 BMessageboundariesRequest andresponse pairingStreamsocketSOCK_STREAMStreamsocketSOCK_STREAMPacketsocketSOCK_SEQPACKETPacketsocketSOCK_SEQPACKETHTTPJSONEdge codedzstd,br,gzipMessageboundariesRequest andresponse pairingTLSTCPEthernet or Wi-Fi framesIPHTTPJSONEdge codedzstd,br,gzipMessageboundariesRequest andresponse pairingTLSTCPEthernet or Wi-Fi framesIPGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaEthernet framesIn a data centreIPGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaEthernet framesIn a data centreIPHTTPJSONDeflate≥1,024 BMessageboundariesRequest andresponse pairingStreamsocketSOCK_STREAMHTTPJSONDeflate≥1,024 BMessageboundariesRequest andresponse pairingStreamsocketSOCK_STREAMGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaPacketsocketSOCK_SEQPACKETGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaPacketsocketSOCK_SEQPACKETGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaUSB bulk transfersGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaUSB bulk transfersGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaAoFramePrefixedL2CAP packetsLE credit based channelGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaAoFramePrefixedL2CAP packetsLE credit based channelGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaGATT RX and TXOne Homa packet per ATT valueL2CAP packetsFixed ATT channel 0x0004GraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaGATT RX and TXOne Homa packet per ATT valueL2CAP packetsFixed ATT channel 0x0004GraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaAoFrameCOBS and CRC-32CGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaAoFrameCOBS and CRC-32CGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaLoRa packetsExplicit headerGraphTPJSONDeflate>128 BMessageboundariesRequest andresponse pairingHomaLoRa packetsExplicit headerNetworkUnix SocketUSBBluetoothSerial LineLoRaNetworkUnix SocketUSBBluetoothSerial LineLoRaPROTOCOL ON NETWORKHTTP(TCP)Serverless on CloudflareGraphTP(Homa)AI ServicesHTTP(TCP)Serverless on CloudflareGraphTP(Homa)AI ServicesPROTOCOL ON UNIX SOCKETHTTP(Unix Socket)GraphTP(Homa)HTTP(Unix Socket)GraphTP(Homa)PATH ON BLUETOOTHL2CAP channelApps and programsGATTWeb BluetoothL2CAP channelApps and programsGATTWeb BluetoothONE CARRIER PER CONNECTION
Network is the live carrier of this connection, with HTTP(TCP), which Cloudflare serves to a Worker of the Serverless pillar: HTTP at L7 and down through L5, JSON at L6, plain from the Worker and coded by Cloudflare's edge, TLS between HTTP and TCP, which encrypts the coded bytes, authenticates the server and makes it HTTPS, TCP at L4, IP at L3 Packaging, under both protocols of Network, and Ethernet or Wi-Fi frames at L2. Cloudflare's network handles HTTP, TLS and the transport below the Worker and hands it each request with the Accept-Encoding br, gzip, which names no deflate. So the Worker marks no answer and returns plain JSON, Cloudflare's edge codes the answer for the client after the Worker returns, with Zstandard, Brotli or gzip by Cloudflare's rules for compression and never with deflate, and the Worker links no deflate. The HTTP server of Aosabi speaks HTTP(TCP) as well, on a server or in a container, and codes as it does on Unix Socket, JSON(deflate) on a route where the app writes compress(), for an answer of ≥1,024 B whose request names deflate, and plain JSON otherwise.Network is the live carrier of this connection, with GraphTP(Homa), the stack of the AI Services pillar inside a data centre: GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, IP at L3 Packaging, under both protocols of Network, and Ethernet frames at L2.Unix Socket is the live carrier of this connection, with HTTP(Unix Socket), a stack of the HTTP server of Aosabi: HTTP at L7 and down through L5, JSON at L6, coded as JSON(deflate) on a route where the app writes compress(), for an answer of ≥1,024 B whose request names deflate, and plain otherwise, since Aosabi codes no gzip, and the stream socket from L4 down through L2, a reliable byte stream inside the kernel of one machine, with no TLS and no TCP. The packet socket of GraphTP(Homa) waits idle beside it.Unix Socket is the live carrier of this connection, with GraphTP(Homa): GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, and a packet socket at L2 under the Homa lane, SOCK_SEQPACKET on Linux, that keeps each Homa packet whole. The stream socket of HTTP(Unix Socket) waits idle beside it.USB is the live carrier of this connection, with GraphTP(Homa), a stack of the Embedded Services pillar: GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, and USB bulk transfers at L2.Bluetooth is the live carrier of this connection, with GraphTP(Homa) on its L2CAP channel, a stack of the Embedded Services pillar: GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, and prefixed AoFrame over L2CAP packets at L2. Some clients read the LE credit based channel as a byte stream, so a 4 byte length marks each Homa packet, and pairing encrypts the link. The channel is the path of apps and programs.Bluetooth is the live carrier of this connection, with GraphTP(Homa) on its GATT path, a stack of the Embedded Services pillar: GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, and GATT at L2, where each Homa packet travels as one ATT value with no AoFrame, written to RX without response and notified on TX, over the ATT channel of L2CAP, the fixed channel 0x0004 of LE. RX and TX require an encrypted link, as the PSM characteristic does, so the stack drives LE Secure Connections pairing and bonding. It is mainly the path of Web Bluetooth in Chromium browsers such as Chrome and Edge. GraphiQL, the page hosted at aosabi.org/dev, reaches a device over it from the browser, and its button aoviewer draws the schema GraphiQL reads. Web Bluetooth runs in Chrome from 56 on macOS and ChromeOS, Chrome from 56 and Samsung Internet from 6.4 on Android, with no WebView, Edge from 79 on Windows and macOS, Chrome on Linux behind the flag #enable-web-bluetooth, and Chrome from 70 on Windows 10 1703, although Windows stays outside the targets of the Bluetooth carrier. Firefox, Safari and the iOS builds of Chrome, Edge and Firefox have no Web Bluetooth, and on iOS the browser apps Bluefy and WebBLE reach the path through a native bridge.Serial Line is the live carrier of this connection, with GraphTP(Homa), a stack of the Embedded Services pillar: GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, and AoFrame at L2, which marks where each Homa packet starts and ends on the byte stream of the UART, with COBS and a CRC-32C.LoRa is the live carrier of this connection, with GraphTP(Homa), a stack of the Embedded Services pillar: GraphTP from L7 down through L6, JSON at L6, coded as one zlib stream of deflate for every body of >128 B in either direction and plain at 128 bytes or fewer, the one coding of GraphTP, Homa at L4 and up through L5, and LoRa packets at L2, whose explicit header states their length, so LoRa is designed to run without AoFrame.

The same request, the same bytes

GraphTP carries the request a client sends over HTTP, and the device is designed to answer it with the code that answers HTTP(TCP). Nothing converts GraphTP back to GraphQL.

A conventional server has no seam

Its app is welded to an HTTP listener, so it answers on Network and Unix Socket and on no other carrier. Aosabi puts the seam between the app and the protocols, where every carrier can reach it.

The presentation, deflate on both protocols

At L6, the presentation, the body is JSON on every stack, and each protocol carries it with its coding itself. HTTP names its coding in its own fields and codes deflate alone, since Aosabi codes no gzip: on a route where the app writes compress(), an answer of ≥1,024 B travels as JSON(deflate) when its request names deflate, and every other answer travels plain. The base pipeline of a Rust service codes no answer, since a coded answer that holds a secret beside text the client chooses lets a reader of the wire guess the secret from its length, and the TypeScript package keeps compress() in its base pipeline. HTTP decodes no request and refuses one with a Content-Encoding with 400. GraphTP has one coding and no choice: every body of >128 B, a request as well as an answer, travels as one zlib stream of deflate, one of 128 bytes or fewer travels plain, and the receiver reads the coding from the compressed bit of a request and the first byte of an answer. A receiver decodes every body back to the same bytes, and the coding shrinks the 597 answers of >128 B of the goldens, the executor corpus and the Japanese capture from 1,628,748 to 294,771 bytes, while the 6 answers of exactly 128 bytes travel plain.

GraphTP codes with a window of at most 4 KiB on every card, and HTTP on a small card such as the ESP32-C3 uses the same window. A small window changes the ratio and never the validity. The table gives the RAM of zlib, and at that window the streaming encoder of the crate graphtp takes 20,742 bytes with small tables and its smallest buffer, 24,576 with small tables and 49,152 with its defaults, as the notebook 37_content_coding measures it, while HTTP on a server card codes at a 32 KiB window.

Two alternatives were measured. CBOR saves about a fifth of the bytes, and its gain in time is limited to large answers over LoRa and, much less, Serial Line. At a 4 KiB window the fast levels of the reference Brotli encoder save less than deflate with more than twice its memory, and its better levels need 0.6 to 1.2 MB. Aosabi codes no gzip since the owner's word of 2026-09-27. A gzip member and a zlib stream carry the same DEFLATE data, and gzip wraps it in 18 bytes where zlib takes 6, so gzip would cost 12 bytes more on every coded answer and about 1.6 KiB of code for its wrapper and its CRC-32, and it compresses neither better nor worse.

On a Cloudflare Worker the rule above finds no deflate, since Cloudflare hands the Worker every request with the Accept-Encoding br, gzip. So an Aosabi Worker marks no answer and returns plain JSON, and by Cloudflare's rules for compression the edge compresses the answer for the client after the Worker returns, with Zstandard, Brotli or gzip, for status 200, 403 or 404 from about 50 bytes where Cloudflare lists its content type, application/json among them and application/graphql-response+json not. The Worker is therefore designed to link no HTTP server, no TLS, no sockets, no deflate and no request decoder.

WindowRAMIntrospectionLarge answers
32 KiB256 KiB87.3%85.0%
4 KiB24 KiB85.9%84.3%
2 KiB12 KiB83.4%82.4%
1 KiB6 KiB78.9%78.5%
Bytes saved against JSON on the introspection answers and on the answers over 1 KiB, and the RAM of the zlib encoder, which follows zlib's formula from the window and the memLevel of each row, 8, 4, 3 and 2 from the top. Five corpus schemas, graphql-js 16.12 and zlib.

Why Homa and GraphTP

Whole messages,
a thin protocol.

Over TCP, a byte stream, HTTP does the session work itself: it marks where each message ends and pairs each response with its request. Over Homa the transport does that work. So HTTP and Homa both claim L5 and do not fit together, which is why Aosabi speaks GraphTP, the Graph Transfer Protocol, which leaves that work to Homa. GraphTP is specified apart from Aosabi, so any GraphQL server and client can implement it.

Homa is a revolutionary transport protocol for the data centre. It was designed from a clean slate. It carries a request and its response as messages instead of a byte stream and needs no connection. The receiver grants whatever follows the first bytes of a message. On a 40 node cluster under high load, the 99th percentile latency of HomaModule, the Linux kernel module of Homa's authors, for short messages is 19 to 72 times lower than that of TCP.

The AI Services pillar keeps GraphTP(Homa) on Network inside a data centre, because Homa is meant for machines that are physically close, and a client across the internet reaches a service over HTTP(TCP). On Network GraphTP(Homa) runs on the crate homa over raw IP 146, and Homa stays the transport there, while QUIC, which was weighed against it, is not taken up. Its speed is unknown until a Jupyter notebook measures it, and the latencies above are the published numbers of HomaModule, which Aosabi uses as its reference. On the Serial Line of an ESP32-C3, simulated in QEMU, a client fetches a song, its artist and the artist's songs over GraphTP(Homa) in one round trip, where REST takes three.

Aosabi also has its own HTTP server, a fork of hyper. Its native binary speaks HTTP/1.1 over TCP and over a Unix socket, and HTTP/2 through TLS 1.3 once a service turns TLS on. It runs in standalone containers and above all is the benchmark that measures what GraphTP(Homa) gains over HTTP. That gain has no number until the benchmark has measured it. HTTP codes with its own fields, identity or deflate and never gzip, and it decodes no request and answers a coded one with 400. GraphTP codes deflate in the zlib format for every body of >128 B, a request as well as an answer, its one coding.

A query or a mutation is one Homa RPC, as Aosabi designs it. Its request carries in GraphTP a head of 3 bytes, the credential and the body a client sends in a POST over HTTP(TCP), query text included, and its response carries a head of 2 bytes, the HTTP status and a substatus such as 200.1 Partial, and the body of the answer over HTTP, errors and introspection included. GraphTP carries its JSON and its compression itself and holds no field and no session: no handshake comes first, and every message stands alone. A subscription opens with one RPC, and each batch of events answers a PULL RPC that the client parks at the server. Among the RPCs in flight, Homa sends the shortest message first, so on a slow link such as Bluetooth a small response is designed to pass a large one.

A client adds GraphTP beside HTTP with one line. The npm package graphtp is designed to give Node.js, Deno, Bun and browsers graphtpFetch, a function with the signature of fetch that takes the POST a GraphQL client builds and returns a Response with the HTTP status, the substatus in the header graphtp-status and the decoded JSON. Apollo Client, urql, graphql-request, GraphiQL and Relay keep their code and change the one line that gives them fetch, beside the import of the function and a URL whose scheme names the carrier. In Apollo Client the line reads:

link: new HttpLink({uri: 'graphtp://orders.example:3000/graphql', fetch: graphtpFetch}),

Learn more on GitHub

HTTP and GraphTP by the byte

The same call,
fewer bytes.

A probe of 2026-09-25 measures four exchanges. It writes and reads back each frame of GraphTP/1 with a prototype of the crate graphtp, and records HTTP/1.1 between the fetch of Node.js 24.13.0 and a GraphQL server on Node.js. The query {__typename} takes 60 bytes of request and answer over GraphTP/1 and 520 over HTTP/1.1, whose request head takes 278 bytes and answer head 187, where GraphTP/1 writes 3 and 2. GraphTP codes a request of >128 B as well, so the introspection request of 2,060 bytes travels in 428, while HTTP/1.1 sends it plain, 2,340 bytes with its head. A content or a body of exactly 128 bytes travels plain and one of 129 bytes coded. The notebook 24_graphtp writes the first three exchanges through the crate graphtp in the same 60, 346 and 175 bytes, and a full introspection document of its own, 1,206 bytes of JSON, travels in 375, while the columns of HTTP keep the numbers of the probe.

HPACK narrows the gap. On a connection in use HTTP/2 finds the fields it has sent before in the dynamic table of HPACK and names each in about a byte, so the same query takes 129 bytes over HTTP/2, and 261 on its first exchange. On Network with IPv4, GraphTP(Homa) takes 212 bytes in two IP packets with the full header of Homa, HTTP/2 on TCP 233 and HTTP/1.1 624, so GraphTP saves 21 bytes against a warm HTTP/2 connection. GraphTP sends the credential in every request and never compresses it, against the attacks CRIME and BREACH, while HPACK indexes it after the first request: with a Bearer token of 71 bytes a repeated {__typename} takes 131 bytes over GraphTP/1 and 130 over HTTP/2. The introspection answer is 19 bytes longer over GraphTP/1 than the whole answer of HTTP/1.1, because GraphTP codes with a window of 4 KiB and the recorded server gzips with one of 32 KiB. The fetch of Node.js names gzip and deflate, so the HTTP server of Aosabi answers it with deflate in the zlib format, whose wrapper takes 6 bytes where the one of gzip takes 18. GraphTP has no handshake, so a first exchange with a new peer costs what the table shows, while HTTP/1.1 on a new TCP connection adds 172 bytes and a round trip and HTTP/2 109 bytes of preface and settings, and TLS adds its own where it runs.

Clients write more than a request needs. RFC 9110 asks a user agent to send a User-Agent field in each request unless it is configured not to. The fetch of Node.js sends User-Agent, Accept-Language and Sec-Fetch-Mode in the recorded request, 60 bytes, and names itself node in 18 of them. Version 2.34.2 of the requests library of Python sends python-requests/2.34.2, a line of 36 bytes, and Chrome on Windows, whose major version has three digits since version 100, sends a line of 125 in the reduced form Chromium documents, on every request. GraphTP carries no field, the user agent among them, and the one value of HTTP's fields it carries is the credential. Naming a coding costs GraphTP no byte: the compressed bit of the request head and the first byte of the body of an answer name it, where Accept-Encoding takes 32 bytes of the recorded request.

The crate graphtp is shaped like the crate http. It holds the types and the codec of GraphTP/1 and nothing that moves a byte, with no standard library, no dependency and no feature, so its zlib encoder and decoder are part of every build. The binding to Homa is the crate graphtp-homa beside it, as hyper stands beside http, and GraphTP is specified apart from Aosabi, so any GraphQL server and client can implement it. A prototype of the crate, built for size, adds 9,695 bytes of code and constant data on riscv32imc-unknown-none-elf, the target of the ESP32-C3, and 11,648 on aarch64. On both targets the two heads with the status mapping stay below 500 bytes, and the encoder takes about two thirds of the codec. That is less than the types of the crate http 1.5.0 alone, 19,664 bytes on aarch64, and less than half of the compression crate miniz_oxide 0.8.9. The crate itself, as the notebook 24_graphtp measures it, takes 19,264 bytes by the object file on riscv32imc and adds 16,040 with its unwind tables to an image on aarch64, where the prototype adds 13,632, and its two heads with the status mapping take 364 and 440 bytes of code. The HTTP/1.1 server path of hyper 1.11.0 takes 110,168 bytes on aarch64 in an earlier probe for the plan of M5, and the server path of the fork of hyper 1.11.1 with its Node connection takes 97,796 bytes on the ESP32-C3, as the notebook 35_esp32c3_http_vs_graphtp measures it in the benchmark build of the card. Measured on an ESP32-C3 simulated in QEMU, the image of Aosabi in Rust with a song app takes 1,037,120 bytes, 0.99 MiB and so under 1 MiB, 9,764 of them for the app, and answers a request for a song, its artist and the artist's songs over GraphTP(Homa) on the Serial Line at 115,200 baud in one round trip, 300 bytes and 32.4 ms of line and computing time, a lower bound for a real chip, since the computing time counts one cycle per instruction. The same app as REST on hyper with HTTP/2 and TLS 1.3 on ESP-IDF takes an image of 858,224 bytes and, timed the same way over plain HTTP/1.1, three round trips, 827 bytes and 72.6 ms.

The service reads the query text fast. GraphTP carries the query text, and the service parses and validates it as it does over HTTP. On one core of an Apple M1 Max the parser of Aosabi, translated from graphql-js 16.12.0, parses about twice as fast as graphql-js since the item LANG2 of the roadmap closed on 2026-10-01: the Feed query in 2.47 µs against 5.85 and the introspection query in 7.08 against 16.81, and one parse of the introspection query allocates 37 times where it allocated 122 before. Since the item PASS1 closed on 2026-10-21 a single pass compiled from the schema takes the seat of the validator by default, and the rules of the validation answer every document it refuses or defers, so every answer stays the same. Japanese text travels byte for byte: kanji stand raw in UTF-8 in string values, variables, descriptions and every JSON body, a line and a column count UTF-16 code units as graphql-js counts them, the 64 Japanese documents of the notebook 02_language parse as graphql-js parses them, and the 11 Japanese bodies of the capture come back byte for byte after decoding, plain or coded.

ExchangeGraphTPHTTP/1.1HTTP/2
{__typename}60520129
A mutation with variables346929536
A partial result with an error175668276
Intro­spection4,3456,2385,814
Bytes of request and answer together, with a peer or on a connection already in use, with no credential and no header of a transport: GraphTP/1 as a prototype of the crate writes and reads its frames, HTTP/1.1 as a relay records it between the fetch of Node.js and a GraphQL server on Node.js, and HTTP/2 repeated, with its fields in the dynamic table of HPACK. A probe of 2026-09-25.
PartRequestAnswer
Start line2417
Host230
Connection2424
Keep-Alive023
Date037
Content-Type3264
Content-Length2020
Accept610
Accept-Language200
Sec-Fetch-Mode220
User-Agent180
Accept-Encoding320
Empty line22
HTTP/1.1 head278187
GraphTP/1 head32
Bytes of each line of the head of HTTP/1.1 with its line end, in the request and the answer of {__typename} as the relay records them, beside the heads GraphTP/1 writes in their place: 3 bytes with the version, the kind, the compressed bit and the length of the credential, and 2 with the status and the substatus. The length of a Homa message gives the length of a body.
Codeaarch64riscv32imc
GraphTP, the two heads and the status mapping272480
GraphTP, the full codec11,6489,695
The crate miniz_oxide 0.8.9, both directions26,74422,489
The crate http 1.5.0, its types19,664Needs std
The crates httparse and http with an answer head of HTTP/1.141,184Needs std
The HTTP/1.1 server path of hyper 1.11.0 on aarch64 and of its fork on riscv32imc110,16897,796
Bytes of code and constant data, .text and .rodata, that each part adds to an image built for size at opt-level z. A probe of 2026-09-25 measures the prototype of graphtp, miniz_oxide and the crate http, and an earlier probe for the plan of M5 measures hyper on aarch64. On riscv32imc the notebook 35_esp32c3_http_vs_graphtp measures the server path of the fork of hyper 1.11.1 with its Node connection in the benchmark build of the card on 2026-10-02. The crate http needs the standard library, which riscv32imc-unknown-none-elf lacks.
ParseAosabigraphql-js
Feed2.475.85
Intro­spection7.0816.81
Feed in Japanese4.168.34
Microseconds to parse each query, the median on one core of an Apple M1 Max, Aosabi with the release profile of its workspace and graphql-js 16.12.0 in the same run, as the notebook 02_language measures them at the close of LANG2 on 2026-10-01. Feed in Japanese is the Feed query with Japanese string values.

Worked example: ESP32-C3

One small board,
the whole GraphQL API.

The built in card esp32c3-devkitm1 is the board ESP32-C3-DevKitM-1, one small RISC-V part with no operating system under it. It carries GraphTP(Homa) on the built in USB serial, on Serial Line and on Bluetooth LE.

The command cargo aosabi build --device esp32c3-devkitm1 compiles the kernel of Aosabi for riscv32imc-unknown-none-elf.

GraphTP carries the GraphQL request a client sends over HTTP, so the board is designed to answer the same API as a server.

The GraphiQL of Aosabi at aosabi.org/dev connects from the browser over HTTP, Bluetooth LE, USB serial or Serial Line, and its button aoviewer draws the schema it reads.

The core of Aosabi is Rust without a garbage collector and without std, so the same core runs on the server, at the edge and on the ESP32.

Three round trips against one

The song app of the talk, with the same data, runs as an image of its own per side on an ESP32-C3 simulated in QEMU. REST asks three times in a row, since the first answer names the artist. Aosabi asks once, with one request over GraphTP.

At 115,200 baud every byte of every request and every answer takes 86.8 µs on the line. Computing, as a lower bound with one cycle per instruction at 160 MHz, takes 6.3 ms for Aosabi and 0.9 ms for REST, and everything else is the line.

REST runs on hyper with HTTP/2 and TLS 1.3 on ESP-IDF and is measured over HTTP/1.1 without TLS, as on Linux. The image of Aosabi takes 0.99 MiB in flash, under 1 MiB, of it the app 10 KiB, and the one of REST 0.82 MiB, of it the app 5 KiB. Beside the app stands everything else, for REST ESP-IDF, hyper and rustls, for Aosabi Homa and GraphTP as well.

ServerRound tripsTimeImage
REST in Rust on hyper372.60.82 MiB
Aosabi in Rust over GraphTP132.40.99 MiB
Proven Measured in QEMU on a simulated ESP32-C3 on 2026-10-29: the time in ms for Song, Artist and their songs up to the last byte, the line and the computing, less is better. REST moves 827 bytes in three round trips, Aosabi 300 bytes in one. The image is the app image in flash. Serial Line on UART0 at 115,200 baud, 8N1.

Benchmark

Time and size,
against REST and graphql-js.

The song app of the talk answers one task, a song, its artist and the artist's songs, on Linux: Aosabi in Rust, REST in Rust on hyper, and the library graphql-js on Bun and on Node.js.

Song, Artist and their songs

The same generator asks every server, one request after the other, with the same data. REST needs three requests, GraphQL one.

Without JIT

Without JIT, JavaScript runs in the interpreter alone. The library graphql-js then takes almost nine times as long on Bun and four and a half times as long on Node.js. Aosabi needs no JIT.

The app itself

What counts is the file that starts the server. The app itself takes 1 to 25 KB everywhere, and the rest is the runtime and the libraries.

ServerTimeSizeApp
REST in Rust, opt-level z23.4820 KB5 KB
REST in Rust, opt-level 318.81.2 MB10 KB
Aosabi in Rust, opt-level z56.51.9 MB15 KB
Aosabi in Rust, opt-level 326.73.2 MB25 KB
Bun with graphql-js, without JIT66893.8 MB1 KB
Bun with graphql-js, with JIT75.093.8 MB1 KB
Node.js with graphql-js, without JIT634119.0 MB3 KB
Node.js with graphql-js, with JIT140119.0 MB3 KB
Proven Measured on Linux on 2026-10-29, the median of five rounds of one session. The time in µs for Song, Artist and their songs, less is better. The size of the executable and what it loads, less is better, and the app inside it. Everything in Rust is built twice, both with std from source and lld: opt-level z is the default and optimised for size, opt-level 3 for speed. REST and Aosabi carry TLS and HTTP/2. The library graphql-js is started twice, with its JIT as usual and without JIT in the interpreter alone, Bun with BUN_JSC_useJIT=0 and Node.js with --jitless.

Why it is rare

Why it is not
usually like this.

Nothing in GraphQL forbids a second carrier. Three habits of the usual implementation do, and the fourth card says where this project stops claiming.

01

The entry point is an HTTP listener

The app answers a plain request object, but server frameworks build that object from HTTP and from nothing else. The seam exists and no other carrier reaches it.

02

The answer travels as text

Every key is spelled out in JSON. Fine over fibre, slow over a link budgeted at twelve kilobytes a second, so a large answer is coded with deflate in the zlib format, on HTTP where the request names deflate and on GraphTP for every body of >128 B.

03

The implementation is heavy

Lexer, parser, syntax tree, validator, schema in memory. Reasonable on a server, heavy for a microcontroller. Every build links all of it, and its size is measured per device, the ESP32-C3 first.

04

Where this stays honest

For one command, four bytes beat any query language and always will. The claim is that one typed, evolvable contract across every link should be available, not that it should be compulsory.

Environment cards

One card
per environment.

An environment card says where a service runs: the target, the carriers and their protocols, what the build compiles and what it emits. Five cards ship built into the command line, and cargo aosabi build --device takes one of them by name, the server card by default.

Each card shows how the environment communicates. The fine print is on the back of each card.

Every fact keeps its marker

ProvenProven marks a fact that a byte, a tool run or a test pins.

DocumentedDocumented marks a fact that a vendor page, a datasheet, an upstream source or the documentation of the project states.

AssumedAssumed marks an inference or a design intent that is still open.

The built in cards

Each ships with the command line

AO·ENV·101

Linux server

The primary target, and the default of the command line.

  • arm64
  • x86-64
  • General build
  • JSON
Carriers
Network with HTTP(TCP) and Unix Socket with HTTP(Unix Socket), with TLS, HTTP/2 through ALPN and HTTP/2 by prior knowledge as opt insDocumented
Connection
Listens. The client opens the connectionAssumed
Build emits
One binary for the host, arm64 or x86-64, and in a release build a stripped copy in target/aosabi/dist/Documented

--device servercargo aosabi build

Download
AO·ENV·101Back face

Linux server

Fine print

Target
The host, aarch64-unknown-linux-gnu or x86_64-unknown-linux-gnu, both upstream tier 1Documented
Executor
General, with the parser, the validator and the introspection resolver, on the project's executor, one thread over epollDocumented
Presentation
On HTTP JSON, and JSON(deflate) on the routes where the app writes compress(), with deflate alone and no gzipDocumented

Built into the command line. Its native binary runs on aarch64 and x86_64.

AO·ENV·102

Linux server, older loader

The server card for a system whose loader is older than glibc 2.36.

  • arm64
  • x86-64
  • General build
  • JSON
Carriers
As the server card, Network and Unix SocketDocumented
Connection
Listens. The client opens the connectionAssumed
Build emits
One binary, which a release build links without packed relocationsDocumented

--device server-legacycargo aosabi build

Download
AO·ENV·102Back face

Linux server, older loader

Fine print

Target
The same two Linux triples as the server cardDocumented
Difference
The release build links without packed relocations, which a loader older than glibc 2.36 does not readDocumented
Executor
General, as the server cardDocumented

Built into the command line. Everything else is the server card unchanged.

AO·ENV·103

Docker container

The server card in a box, with nothing new to learn.

  • Container image
  • Port 3000
  • Non root
Carriers
Network and Unix SocketDocumented
Connection
Listens on port 3000 as the user nonroot. The client opens the connectionDocumented
Build emits
A container image for the architecture of the host, a multi stage build onto a distroless runtimeDocumented

--device dockercargo aosabi build

Download
AO·ENV·103Back face

Docker container

Fine print

Target
The host, as the server cardDocumented
Images
The slim Rust image of the pinned toolchain builds, and the distroless image with the C runtime runs, both pinned by digestDocumented
Executor
General, as the server card, on the project's executorDocumented
Presentation
JSON, and JSON(deflate) on the routes where the app writes compress(), with deflate alone and no gzipDocumented

Built into the command line. It builds the server package of the server card, always in the release profile, and podman or docker builds the image.

AO·ENV·104

Cloudflare Worker

No sockets of its own. The platform calls it, and it may call out.

  • WebAssembly
  • No inbound sockets
  • JSON
Carriers
Network with HTTP(TCP), through the fetch entry of the platformDocumented
Connection
Never accepts a socket. The platform hands it each request, and the client opens every connectionDocumentedAssumed
Build compiles
The library of the service for the platform, with no crate and no symbol of the HTTP engineDocumented

--device workercargo aosabi build

Download
AO·ENV·104Back face

Cloudflare Worker

Fine print

Target
The triple wasm32-unknown-unknownDocumented
Platform
No threads, no file system, no sockets, no process clock. Futures run on the host job queueDocumented
Executor
General, on the host job queueDocumented
Presentation
Plain JSON from the Worker, coded for the client by the edge of the platformDocumentedAssumed

Built into the command line for a service in Rust. A TypeScript app runs on the engine built as core.wasm, and in workerd a song app of that kind takes 45.8 ms from a cold start to its first answer, the median of 30 starts, measured on 2026-10-29.

AO·ENV·105

ESP32-C3, bare metal

No operating system under it. The board ESP32-C3-DevKitM-1 with one small RISC-V part.

  • RISC-V
  • no_std
  • GraphTP(Homa)
  • JSON
Carriers
The built in USB serial, Serial Line and Bluetooth LE, each with GraphTP(Homa)Documented
Radio
Needs the binary driver of the vendor, even with no operating systemDocumented
Target
The triple riscv32imc-unknown-none-elf, upstream in RustDocumented
Build compiles
The kernel crates aosabi-kernel and aosabi-http for the target, with no stdDocumented

--device esp32c3-devkitm1cargo aosabi build

Download
AO·ENV·105Back face

ESP32-C3, bare metal

Fine print

Hardware layer
Bare metal with esp-hal, no ESP-IDF and no FreeRTOSDocumented
Executor
Embassy, with one allocator of the card over a fixed arenaDocumented
Presentation
On GraphTP JSON, one zlib stream for every body of >128 B, with a 4 KiB windowDocumented

Built into the command line. Measured for the talk on 2026-10-29 in Espressif QEMU 9.2.2, machine esp32c3: an image of a song app, built apart from the card, takes 1,037,120 bytes, under 1 MiB, and answers one request for a song, its artist and the artist's songs over GraphTP(Homa) on the Serial Line in 32.4 ms, a lower bound for a real chip.

Tux holding the Carinthian coat of arms, the logo of the Carinthian Linuxday

Carinthian Linux Day

Kärntner Linuxtåg 2026,
Aosabi: GraphQL everywhere.

At the Kärntner Linuxtåg 2026, the Carinthian Linux Day in Villach, Florian Kleber gave the talk Aosabi: GraphQL everywhere, on one GraphQL server from the microcontroller to the edge.

Friday 30 October 2026, 15:00

The talk was held at the IT-Gymnasium St. Martin, St. Martiner Straße 7, 9500 Villach.

The talk

The schema from the code, validation in a single pass in Rust, a benchmark against REST and graphql-js, and two demos, an ESP32-C3 and a Cloudflare Worker. The slides are online.

Silver sponsor

Netsnek is a silver sponsor of the Kärntner Linuxtåg and helps bring the Linux days to Carinthia. Aosabi is a Netsnek project.