Expand description
§Moonbeam
A single-threaded-first async HTTP/1.1 server written in Rust.
Moonbeam is designed to be simple, efficient, and free of synchronization overhead by running on a single thread. It leverages the async-io and smol ecosystem to handle concurrent connections asynchronously. By default, it uses a “share-nothing” architecture, avoiding the need for Arc, Mutex, or Send/Sync bounds on your state, though it can easily be extended to multiple threads if desired.
§Motivation
Modern web applications often spend most of their time waiting on I/O (databases, network requests, etc.) rather than performing heavy CPU computation. Moonbeam embraces this by running your application logic on a single thread, utilizing a local executor. This means you can use simple RefCell and Cell primitives for state management, drastically reducing the cognitive overhead and boilerplate often associated with multi-threaded Rust web frameworks.
§Critical Considerations
Before building with Moonbeam, it’s essential to understand its execution model:
- No Tokio: Moonbeam is built on
async-ioand thesmolecosystem. It does not usetokiodependencies. This means notokio::spawn, no#[tokio::main], and no tokio-specific database drivers (unless they supportasync-ioorsmol). - Blocking I/O: Because Moonbeam runs handlers on a
LocalExecutoron the main thread, any CPU-heavy computation or blocking I/O (like reading a large file synchronously) will block the entire server.- Solution:
smolsupports async I/O via theblocking::unblockprimitive for offloading heavy tasks to a background thread pool, or you can use theasync_iocrate for native non-blocking operations.
- Solution:
§Features
- Single-threaded by default: No
ArcorMutexneeded for shared state. - Multi-threaded support: The
mtfeature spawns worker threads, each with its own state copy. - Simple API: Use the
#[server]macro to turn functions into server handlers. - Routing: The
router!macro provides a clean DSL and efficient implementation for nested groups, middleware, path parameters, and wildcards. - Typed Body Extractors: Use
FromRequestandFromBodytraits for zero-copy, asynchronous body parsing (e.g., JSON). - Static Assets: Built-in
assetshelper for serving files with ETags and MIME type detection. - HTTP/1.1: Persistent connections, chunked transfer encoding, and standard header parsing.
- Zero-cost extractions: Efficient parsing of Cookies, Query Parameters, and Bodies.
- Panic Handling: Optional
catchpanicfeature safely catches panics and returns a 500 error. - Response Compression: On-the-fly
compresssupport (Gzip, Brotli, Zlib). - Graceful Shutdown: Intercepts
signalsfor clean exit. - TLS Support: Secure your server with
rustls(behind thetlsfeature).
§Is it fast?
Yes. Moonbeam is designed for high performance with minimal overhead. In simple benchmarks using wrk (4 threads, 100 connections, 5 seconds), Moonbeam shows competitive performance for both simple responses and static file serving.
The below benchmarks were performed on a MacBook Pro (M3 Pro). While these simple tests don’t represent real-world application complexity, they demonstrate the efficiency of Moonbeam’s core request/response loop.
§Hello World (Plain Text)
| Framework | Architecture | Requests/sec |
|---|---|---|
| Axum (Tokio) | Multi-threaded | ~216,000 |
| Moonbeam | Multi-Threaded (4 cores) | ~214,000 |
| Moonbeam | Single-Threaded | ~211,000 |
| Node.js | Single-Threaded | ~117,000 |
| Rouille | Thread-per-connection | ~93,000 |
§Static File Serving (4KB file)
| Framework | Architecture | Requests/sec |
|---|---|---|
| Moonbeam | Multi-Threaded (4 cores) | ~73,000 |
| Moonbeam | Single-Threaded | ~66,000 |
| Axum (Tokio) | Multi-threaded | ~58,000 |
| Rouille | Thread-per-connection | ~56,000 |
| Node.js | Single-Threaded | ~51,000 |
§Build Times & Install Size
Moonbeam also offers relatively fast compilation times and a small disk footprint. For the simple benchmarks:
| Framework | Clean Build Time | Install Size (Avg) |
|---|---|---|
| Moonbeam | ~4.6s | ~926 KB |
| Axum (Tokio) | ~8.6s | ~1.6 MB |
| Rouille | ~8.9s | ~933 KB |
| Node.js | 0s | ~78 MB |
Note: The Node.js benchmark does not require any build step and relies entirely on built-in standard library modules. However, the Node.js runtime itself typically requires an installation footprint of around 50–100 MB depending on the platform.
§Installation
Add moonbeam to your Cargo.toml:
[dependencies]
moonbeam = "0.7"§Feature Flags
Moonbeam is configurable via Cargo features. Most users will want the default features.
default: Enablesmacros,assets,catchpanic,signals, androuter.macros: Enables the#[server]attribute macro to easily createServertrait implementations.assets: Exposes themoonbeam::assetsmodule for serving static files.signals: Hooks into OS signals (SIGINT, SIGTERM) to trigger graceful server shutdown.catchpanic: Wraps your handlers to catch panics gracefully and return500 Internal Server Error.tracing: Instruments the core server loop withtracingspans and events.compress: Enables automatic response compression. (Depends onflate2andbrotli).router: Enables the routing macros (#[route],#[middleware], androuter!).mt: Exposesserve_multito run multiple independent server isolates across available CPU cores.tls: Enables HTTPS support viarustls. Exposesserve_tls,serve_multi_tls, andTlsConfig.
§Configuration
Moonbeam honors the following environment variables:
MOONBEAM_MAX_BODY_SIZE: Maximum size (in Kilobytes) of an incoming HTTP request body. Defaults to1024(1MB). Exceeding this returns a413 Content Too Large.
§Examples
§Stateless Server
The simplest way to use Moonbeam.
use moonbeam::{Body, Request, Response, Spawner, server};
#[server(HelloWorld)]
async fn serve(_request: Request, _spawner: Spawner) -> Response {
Response::ok().with_body("Hello, World!", Body::TEXT)
}
fn main() {
println!("Running on 127.0.0.1:8080");
moonbeam::serve("127.0.0.1:8080", || HelloWorld);
}§Stateful Server (Interior Mutability)
Because the executor runs locally, you can use std::cell::Cell without Mutex.
use std::cell::Cell;
use moonbeam::{Body, Request, Response, Spawner, server};
struct AppState {
count: Cell<u64>,
}
#[server(CounterServer)]
async fn serve(_req: Request, _spawner: Spawner, state: &AppState) -> Response {
let count = state.count.get();
state.count.set(count + 1);
Response::ok().with_body(format!("Request #{}", count), Body::TEXT)
}
fn main() {
let state = AppState { count: Cell::new(0) };
moonbeam::serve("127.0.0.1:8080", move || CounterServer(state));
}§Avoiding Macros
If you prefer not to use macros, the same examples above can be easily achieved with only slightly
more boilerplate. Make sure to update Cargo.toml to disable default features:
[dependencies]
moonbeam = { version = "0.7", default-features = false }use std::cell::Cell;
use moonbeam::{Body, Request, Response, Spawner, Server};
struct CounterServer {
count: Cell<u64>,
}
impl Server for CounterServer {
async fn route<'server: 'exec, 'exec>(
&'server self,
_req: Request<'_, '_>,
_spawner: Spawner<'exec>,
) -> Response {
let count = self.count.get();
self.count.set(count + 1);
Response::ok().with_body(format!("Request #{}", count), Body::TEXT)
}
}
fn main() {
moonbeam::serve("127.0.0.1:8080", || CounterServer { count: Cell::new(0) });
}§Multi-threaded “Share-Nothing” Server
Use the mt feature flag to scale across multiple CPU cores.
use moonbeam::{Request, Response, ThreadCount, Body, Spawner, server, serve_multi};
use std::sync::atomic::{AtomicUsize, Ordering};
struct WorkerState {
thread_id: usize,
}
#[server(Worker)]
async fn serve(_req: Request, _spawner: Spawner, state: &WorkerState) -> Response {
Response::ok().with_body(format!("Hello from thread {}", state.thread_id), Body::TEXT)
}
fn main() {
// Shared setup logic (runs once on the main thread)
let next_id = AtomicUsize::new(0);
serve_multi(
"127.0.0.1:8080",
ThreadCount::Default, // One thread per CPU core
|| {
// This closure runs on each new thread to construct its local state
let id = next_id.fetch_add(1, Ordering::Relaxed);
Worker(WorkerState { thread_id: id })
}
);
}§HTTPS Server (TLS)
Secure your server using the tls feature.
use moonbeam::{Body, Request, Response, Spawner, server, TlsConfig, serve_tls};
#[server(HelloWorld)]
async fn serve(_request: Request, _spawner: Spawner) -> Response {
Response::ok().with_body("Hello, Secure World!", Body::TEXT)
}
fn main() {
let tls_config = TlsConfig::from_pem("cert.pem", "key.pem")
.expect("Failed to load TLS certificates")
.into_server_config()
.expect("Failed to create server config");
println!("Running HTTPS on 127.0.0.1:4433");
serve_tls("127.0.0.1:4433", tls_config, || HelloWorld);
}§Advanced Routing
The router! macro provides a clean domain-specific language for nesting routes and middleware.
use moonbeam::{Body, Request, Response, Spawner, route, router, serve, middleware};
use moonbeam::router::PathParams;
struct AppState {
api_key: String,
}
// Global Middleware
#[middleware]
async fn logger(req: Request, _spawner: Spawner, _state: &AppState, next: Next) -> Response {
let start = std::time::Instant::now();
let res = next(req).await;
println!("{} {} - {:?}", req.method, req.path, start.elapsed());
res
}
// Scoped Middleware
#[middleware]
async fn require_auth(req: Request, _spawner: Spawner, state: &AppState, next: Next) -> Response {
if req.find_header("X-Api-Key") == Some(state.api_key.as_bytes()) {
next(req).await
} else {
Response::new_with_code(401).with_body("Unauthorized", Body::TEXT)
}
}
// Extractor Handler
#[route]
async fn get_user(PathParams(id): PathParams<&str>) -> Response {
Response::ok().with_body(format!("User ID: {}", id), Body::TEXT)
}
#[route]
async fn not_found() -> Response {
Response::new_with_code(404).with_body("Not Found", Body::TEXT)
}
fn main() {
router!(ApiRouter<AppState> {
with logger
"/api" => {
with require_auth
get("/users/:id") => get_user,
// Unmatched /api/* routes to default 404
_ => !
}
// Custom 404
_ => not_found
});
let state = AppState { api_key: "secret".to_string() };
serve("127.0.0.1:8080", move || ApiRouter::new(state));
}§JSON Parsing (Typed Body Extraction)
Use the moonbeam-serde crate for flexible, typed body extraction. This supports zero-copy deserialization by borrowing directly from the request buffer.
use moonbeam::{Response, route, router, serve};
use moonbeam_serde::Json;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User<'a> {
id: u32,
name: &'a str, // Borrowed from the request body
}
#[route]
async fn create_user(Json(user): Json<User<'_>>) -> Json<User<'_>> {
println!("Creating user: {:?}", user);
Json(user)
}
fn main() {
router!(ApiRouter {
post("/users") => create_user
});
serve("127.0.0.1:8080", || ApiRouter);
}§HTML Forms (URL-Encoded and Multipart)
Use the moonbeam-forms crate to parse incoming form data, including file uploads.
use moonbeam::{Body, Response, route, router, serve};
use moonbeam_forms::{FormData, Form};
#[route]
async fn handle_form(form: Form<'_>) -> Response {
let mut response_text = String::new();
// Find a specific field (iterator for multiple values)
for data in form.find("username") {
if let FormData::Text(name) = data {
response_text.push_str(&format!("Hello, {}!\n", name));
}
}
// Handle file uploads
for data in form.find("profile_pic") {
if let FormData::File { name, content_type, data } = data {
response_text.push_str(&format!(
"Received file: {} ({}) - {} bytes\n",
name, content_type, data.len()
));
}
}
Response::ok().with_body(response_text, Body::TEXT)
}
fn main() {
router!(App {
post("/submit") => handle_form,
// GET params are also accessible via Form
get("/submit") => handle_form
});
serve("127.0.0.1:8080", || App);
}§Serving Static Files
use moonbeam::{Request, Response, Spawner, server, assets::get_asset};
#[server(StaticServer)]
async fn serve(req: Request, _spawner: Spawner) -> Response {
let etag = req.find_header("If-None-Match");
get_asset(req.path, etag, "./public").await
}
fn main() {
moonbeam::serve("127.0.0.1:8080", || StaticServer);
}§License
This project is licensed under the MIT License.
Re-exports§
pub use crate::http::Body;pub use crate::http::Request;pub use crate::http::Response;pub use crate::server::mt::serve_multi_tls;mtandtlspub use crate::server::mt::ThreadCount;mtpub use crate::server::mt::serve_multi;mtpub use crate::server::task::Executor;pub use crate::server::task::Spawner;pub use crate::server::Server;pub use crate::server::st::serve;pub use crate::server::st::serve_tls;tlspub use crate::server::tls::TlsConfig;tls
Modules§
Macros§
- router
router - Defines a router and its routing tree.
- spawn_
with_ span - Spawns a task onto the executor, instrumenting it with a child span that inherits the current tracing context.
Structs§
- Header
- Represents a parsed header.
Attribute Macros§
- middleware
router - Defines a middleware function for use in a
router!. - route
router - Defines a route handler for use with the
router!macro. - server
macros - Attribute macro to simplify creating server implementations.
Attribute macro to convert a function into a
Serverimplementation.