{"id":23552335,"url":"https://github.com/intob/peregrine","last_synced_at":"2025-10-28T08:02:00.347Z","repository":{"id":269769196,"uuid":"908409102","full_name":"intob/peregrine","owner":"intob","description":"A high-performance HTTP server, written from scratch in Zig. Built on kqueue/epoll.","archived":false,"fork":false,"pushed_at":"2025-01-25T23:14:35.000Z","size":228,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-15T19:56:32.349Z","etag":null,"topics":["iovec","zero-alloc","zero-copy"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/intob.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-26T02:24:13.000Z","updated_at":"2025-01-19T18:56:07.000Z","dependencies_parsed_at":"2024-12-26T03:23:51.235Z","dependency_job_id":"57aff98a-f9ce-4797-8e44-95baafe6bbf1","html_url":"https://github.com/intob/peregrine","commit_stats":{"total_commits":40,"total_committers":1,"mean_commits":40.0,"dds":0.0,"last_synced_commit":"c03b5fb32582582f724b677d67f70094cd6ad4c1"},"previous_names":["intob/peregrine"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/intob%2Fperegrine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/intob%2Fperegrine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/intob%2Fperegrine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/intob%2Fperegrine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/intob","download_url":"https://codeload.github.com/intob/peregrine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239293946,"owners_count":19615041,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["iovec","zero-alloc","zero-copy"],"created_at":"2024-12-26T11:10:38.553Z","updated_at":"2025-10-28T08:01:55.309Z","avatar_url":"https://github.com/intob.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Peregrine - a simple high-performance HTTP server\nThis is an event-driven HTTP server. Written in pure Zig, with no dependencies other than Zig's standard library. Supports Linux (epoll) and BSD/MacOS (kqueue) systems.\n\nThe main goal of this project is to provide a HTTP server with the following priorities (in order of prevalence):\n- Reliability\n- Performance\n- Simplicity\n\nCurrently, all heap allocations are made during startup. Internally, no heap allocations are made per-request unless pre-allocated buffers overflow.\n\nNote: This project has just started, and is not yet a complete HTTP server implementation. See [To do section](#to-do). The API is likely to change. It is currently NOT FIT FOR PRODUCTION use.\n\n## Features\n\n- Simple API\n- Support for HTTP/1.0 and HTTP/1.1\n- Support for WebSockets\n- Cross-platform IO Multiplexing\n    - Kqueue support for BSD and MacOS systems\n    - Epoll support for Linux systems\n- Multi-Worker Architecture\n    - Automatic worker scaling based on CPU core count\n    - Configurable worker-thread count\n    - Configurable accept-thread count\n    - Thread-safe request handling\n\n## Benchmarks\nRun using wrk on an M2 Pro with 1000 connections.\n\n| Metric | NGINX | h2o | Zap (Facil.io) | Peregrine |\n|--------|-------|-----|----------------|-----------|\n| Requests/sec | 81,852 | 149,267 | 167,963 | **175,675** |\n| Avg Latency | 12.19ms | 8.54ms | **4.84ms** | 5.64ms |\n| Latency Stdev | 4.19ms | 8.87ms | 2.23ms | 412.57μs |\n| Latency +/- Stdev | 74.74% | 90.81 | 78.32% | **93.00%** |\n\nFacil.io is still superior to this library as it is battle-tested, production-ready and supports TLS. While working on this library, I've seen just how well-implemented and stable Facil.io is...\n\nNote that this benchmark simply measured serving static GET requests, and they do not indicate real-world performance unless you're only serving static files. For these tests, the response length was 6150 bytes.\n\n## Performance optimisations\n\n- Non-blocking socket operations\n- Event-driven architecture\n- Aligned buffer allocation\n- SIMD operations\n- Vectored IO writes the response with a single syscall\n- Zero heap allocations per-request\n- Header case-insensitivity handled when searching, not parsing (unused headers are not transformed)\n- Query only parsed on demand\n- Optimised header, method and version parsing\n- Fixed sized array for headers (faster than std.ArrayList)\n\n## Architecture\n\n### Server\n- Server configuration and initialization\n- Worker pool management\n- Signal handling for graceful shutdown\n- Platform-specific I/O handlers\n\n### Worker threads\n- Manage connections\n- Parse and handle requess\n- Serialise and write responses\n\n### Accept threads\n- Accept connections\n- Set client socket options\n- Assign each client to the next worker\n- Monotonically increment the next_worker counter\n\n### WebSocket threads\n- Join immediately if there are no WS handlers\n- Manage WebSocket connections\n- Parse and handle WebSocket frames\n\n### Signal handling\nThe server handles the following signals for graceful shutdown:\n- SIGINT (Ctrl+C)\n- SIGTERM\n\n## Usage\n\n### Run the counter example server natively\n```bash\nzig build run-counter\n```\n\n### Run the counter example in a Linux Docker container\n\n#### x86_64\n```bash\nzig build -Dtarget=x86_64-linux-musl \u0026\u0026 \\\ndocker build -t counter . -f ./example/counter.Dockerfile \u0026\u0026 \\\ndocker run -p 3000:3000 counter\n```\n#### aarch64 (ARM)\n```bash\nzig build -Dtarget=aarch64-linux-musl \u0026\u0026 \\\ndocker build -t counter . -f ./example/counter.Dockerfile \u0026\u0026 \\\ndocker run -p 3000:3000 counter\n```\n\n### Implement a server\n```zig\nconst std = @import(\"std\");\nconst per = @import(\"peregrine\");\n\nconst Handler = struct {\n    const Self = @This();\n\n    allocator: std.mem.Allocator,\n\n    pub fn init(allocator: std.mem.Allocator) !*Self {\n        const self = try allocator.create(Self);\n        self.allocator = allocator;\n        return self;\n    }\n\n    pub fn deinit(self: *Self) void {\n        self.allocator.destroy(self);\n    }\n\n    pub fn handleRequest(_: *Self, _: *per.Request, resp: *per.Response) void {\n        _ = resp.setBody(\"Kawww\\n\") catch {};\n        resp.addNewHeader(\"Content-Length\", \"6\") catch {};\n    }\n};\n\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    defer _ = gpa.deinit();\n    const srv = try per.Server(Handler).init(gpa.allocator(), 3000, .{});\n    std.debug.print(\"listening on 0.0.0.0:3000\\n\", .{});\n    try srv.start(); // Blocks if there is no error\n}\n\n```\n\nUsing Zig's comptime metaprogramming, the Server is compiled with your handler interface. Simply implement the `init`, `deinit` and `handle` methods. Compile-time checks have your back.\n\nThe configuration is minimal, with reasonable defaults. Simply provide an allocator and port number. Optionally set parameters in the configuration struct.\n\nThe server will shutdown gracefully if an interrupt signal is received. Alternatively, you can call `Server.shutdown()`.\n\n## Memory management model\n\n### Request lifecycle\nThe server manages request and response buffers internally, reusing them across requests to avoid allocations. When a handler processes a request, it must copy any data it needs to retain, as the underlying buffers will be reused for subsequent requests.\n\n### Handler responsibilities\n- Handlers own and manage their internal memory\n- Any data extracted from requests must be copied before the handler returns\n- Handlers are shared across multiple worker threads\n\n## Need to know\n\n### Memory usage\nEach worker thread gets it's own resources (obviously). By default, the number of worker threads is equal to the number of CPU cores. Therefore, the memory usage will depend on allocated buffer sizes, stack sizes, and the number of worker threads. You can adjust these parameters in the configuration struct. Example memory usage:\n```\nWorkers:    Response buffer:    10MB\n            Request buffer:     1MB\n            Stack size:         1MB\n            Overhead:           ~70KB\n\n            Total:              12.07MB\n_______________________________________________________________________\n            Worker count:       12\n            Main thread stack:  ~1MB (currently dependent on the OS)\n            Total:              12.07 * 12 + 0.1 + 0.2 = 145.14MB\n```\nThanks to the zero-allocation design, even under high load, these numbers won't change beyond what your handler allocates.\n\n### Response headers\nIf the body is not zero-length, you must set the Content-Length header yourself. I will write a helper for this soon.\n\nRegardless of whether it's an ArrayList or a HashMap, checking if it was set already by the user would incur a cost (albeit small).\n\nIf you do not give a response body, the \"content-length: 0\" header will be added automatically. This is because it's faster to add a hard-coded iovec than to serialise an extra header.\n\nAgain, this library is designed to be reliable, performant, and simple. Occasionally simplicity is sacrificed for performance.\n\nConnection and keep-alive headers are set by the Worker. This is because there is internal logic to handle connection persistence, and it would hurt developer experience to not set these headers appropriately.\n\n### Query params\n`req.parseQuery()` returns `!?std.StringHashMap([]const u8)` - an error union containing an optional hash map. The semantics are:\n- Returns error.OutOfMemory if hash map insertion fails\n- Returns null in two cases:\n    - No query string exists (no '?' in path)\n    - Malformed query string (missing '=' between key-value pairs)\n- Returns the populated hash map on successful parsing\n\nThe `Request.query` hash map is cleared on the call to `parseQuery()`. Accessing the query field without first calling `parseQuery()` will expose stale data from previous requests. Emptying the hash map has a cost, and we should only pay that price if we want to use the query, not unconditionally for each request.\n\nExample usage:\n```zig\nif (try req.parseQuery()) |query| {\n    // use query hash map\n}\nif (req.query.get(\"some-key\")) |value| {\n    // it's safe to access the map directly after calling parseQuery\n}\n```\n\n### WebSockets\nImplementing WebSockets is easy. Simply handle the upgrade request, and add the WS handler hooks. Check out the working example in `./example/websocket`.\n```zig\npub fn handleRequest(self: *Self, req: *per.Request, resp: *per.Response) void {\n    if (std.mem.eql(u8, req.getPath(), \"/ws\")) {\n        // You must explicitly handle the upgrade to support websockets.\n        per.ws.upgrader.handleUpgrade(self.allocator, req, resp) catch {};\n        return;\n    }\n    self.dirServer.serve(req, resp) catch {};\n}\n\npub fn handleWSConn(_: *Self, fd: posix.socket_t) void {\n    std.debug.print(\"handle ws conn... {d}\\n\", .{fd});\n}\n\npub fn handleWSDisconn(_: *Self, fd: posix.socket_t) void {\n    std.debug.print(\"handle ws disconn... {d}\\n\", .{fd});\n}\n\npub fn handleWSFrame(_: *Self, fd: posix.socket_t, frame: *per.ws.Frame) void {\n    // Reply to the client\n    per.ws.writer.writeMessage(fd, \"Hello client!\", false) catch |err| {\n        std.debug.print(\"error writing websocket: {any}\\n\", .{err});\n    };\n}\n```\n\n## I need your feedback\nI started this project as a way to learn Zig. As such, some of it will be garbage. I would value any feedback.\n\n## This is no framework\nThis is not a framework for building web applications. This is purely a HTTP server designed from the ground up to be stable and performant. There are no built-in features such as routing or authentication. There are some utilities for common use-cases, such as serving a directory of static files, `util.DirServer`.\n\nIf you want a more substantial HTTP library, I suggest that you look at [Zap](https://github.com/zigzap/zap), built on [Facil.io](http://facil.io). Facil.io is an excellent battle-tested library written in C.\n\n## To do\nUntil these things are done, I don't think that this project can possibly be considered production-ready.\n\n- TLS 1.3 support\n- Handle request body\n- Implement better worker selection than round-robin\n- Set Worker thread CPU affinity\n- Make WebSocket component multi-threaded\n- Benchmark connection-pooling (will it improve response times under load?)\n- API reference\n- Add a response helper to set content-length header from an integer. Maybe use a pre-allocated buffer that can be reused.\n\n## Nice to have\n- HTTP/2 support\n- HTTP/3 support\n- Windows support\n- Templating util (possibly adapt util.DirServer to be composable)\n\n## Thanks\nThank you to [Bo](https://github.com/boazsegev) for his advice (not all applied yet), and also for his library [Facil.io](https://facil.io). This served as a great model for robust server design, and a solid performance benchmark. The more I work on this project, the more I've come to appreciate Facil.io's astonishing performance under load.\n\nAlso, thanks to Karl Seguin for his excellent guide to [writing TCP servers in Zig](https://www.openmymind.net/TCP-Server-In-Zig-Part-1-Single-Threaded/). The start of this project was an exercise in learning Zig, and I found this guide to be very helpful for getting started.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fintob%2Fperegrine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fintob%2Fperegrine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fintob%2Fperegrine/lists"}