Skip to content

jensyt/moonbeam

Repository files navigation

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-io and the smol ecosystem. It does not use tokio dependencies. This means no tokio::spawn, no #[tokio::main], and no tokio-specific database drivers (unless they support async-io or smol).
  • Blocking I/O: Because Moonbeam runs handlers on a LocalExecutor on the main thread, any CPU-heavy computation or blocking I/O (like reading a large file synchronously) will block the entire server.
    • Solution: smol supports async I/O via the blocking::unblock primitive for offloading heavy tasks to a background thread pool, or you can use the async_io crate for native non-blocking operations.

Features

  • Single-threaded by default: No Arc or Mutex needed for shared state.
  • Multi-threaded support: The mt feature 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 FromRequest and FromBody traits for zero-copy, asynchronous body parsing (e.g., JSON).
  • Static Assets: Built-in assets helper 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 catchpanic feature safely catches panics and returns a 500 error.
  • Response Compression: On-the-fly compress support (Gzip, Brotli, Zlib).
  • Graceful Shutdown: Intercepts signals for clean exit.
  • TLS Support: Secure your server with rustls (behind the tls feature).

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: Enables macros, assets, catchpanic, signals, and router.
  • macros: Enables the #[server] attribute macro to easily create Server trait implementations.
  • assets: Exposes the moonbeam::assets module 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 return 500 Internal Server Error.
  • tracing: Instruments the core server loop with tracing spans and events.
  • compress: Enables automatic response compression. (Depends on flate2 and brotli).
  • router: Enables the routing macros (#[route], #[middleware], and router!).
  • mt: Exposes serve_multi to run multiple independent server isolates across available CPU cores.
  • tls: Enables HTTPS support via rustls. Exposes serve_tls, serve_multi_tls, and TlsConfig.

Configuration

Moonbeam honors the following environment variables:

  • MOONBEAM_MAX_BODY_SIZE: Maximum size (in Kilobytes) of an incoming HTTP request body. Defaults to 1024 (1MB). Exceeding this returns a 413 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.

About

Single-threaded first async web server for Rust

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Contributors