How GioJS Works
Rust owns the hot path. Node does what it is best at: rendering React.
GioJS splits responsibilities across two layers. The compiled Rust server handles everything performance-critical; Node handles React SSR and the npm ecosystem.
- Rust - HTTP/2, TLS, routing, compression, image optimization, ISR cache, static files, middleware
- Node - React rendering via renderToReadableStream, getServerSideProps, your app logic
A request only reaches Node if it is a dynamic SSR route that missed the cache. Everything else is served entirely from Rust.
The life of a request
Every request passes the same layers in the Rust server, in this order. A layer that can answer on its own - a refusal, a redirect, a cache hit - does, and nothing after it runs:
- Client identity - the client's address and scheme are resolved (through
[server] trusted_proxies), the request gets itsX-Request-Id, and repeatedcookiefields are joined. - Path hygiene - the path is made canonical; dot segments, a raw
\or a broken%escape get400, and unknown/_gio/paths404. - Locale - with
[i18n], the locale is detected and its prefix removed from the path. - Deployment skew - a client navigation from another deployment gets
409and reloads in full. - Rate limits -
[[rate_limits]]rules answer429past their budget. - CSRF - a cross-site
POST,PUT,PATCHorDELETEgets403, before its body is read. - Rules - guards, redirects and rewrites from
gio.tomlandmiddleware.ts. - Prefetch budget - prefetch requests past the per-client budget get
429. - Routing - built-in
/_gioendpoints, build assets, fonts andpublic/files are served from Rust; a page or route handler goes to the page cache, and only on a miss (or for anything personal) to a Node worker.
On the way out, the response gets its security headers (and CSP nonces), its X-Gio-Cache status and its compression. Each of these protections has a switch in gio.toml; see Turning Protections On and Off.
Node workers
The Rust server spawns the Node side itself and talks to it over a local socket (a Unix socket, or a named pipe on Windows) carrying length-prefixed JSON frames. Each worker proves it was started by this server with a per-worker token before it gets any traffic. The server supervises its workers: one that crashes is respawned with backoff, and only the requests it had in flight fail (with a 503) - cached and static content keeps serving throughout.
By default there is one worker. With [server] workers = N (or "auto") the server runs a pool, so renders use several CPU cores:
- Dispatch - each request goes to the ready worker with the fewest requests in flight (open streams included), ties taken in turn. A worker that is respawning is skipped. Everything a request starts - a streamed body, an SSE stream, a Partial Prerendering hole render - stays on its worker.
- One build - the first worker bundles the client code and records the result in
.gio/build/manifest.json; the rest start once it is ready and load that manifest, so they never race on the same files. A worker that cannot load it fails its boot and is retried; it never rebuilds under the workers serving. - WebSockets - each connection is pinned to one worker, which runs its handler. Rooms live in the Rust server, so
broadcast(room, ...)from any worker reaches sockets on all of them. - Shared server state - the page cache, revalidation, middleware rules and rate limits live in Rust, so they behave the same whichever worker handles a request. Module-level state in your app is per worker, and Node plugins'
onStartup/onShutdownhooks run in every worker.
See [server] for the setting and Deployment for sizing a pool.
Further reading
- The Rust ⇄ Node Boundary - the protocol between the server and its workers
- Caching Layers - the page cache and partial prerendering
- Streaming - how streamed pages, route bodies and events cross
- Known Limitations