# Kemal: a Crystal web framework with routing, WebSockets and a 405 that respects RFC 9110

> Kemal is a small HTTP framework for Crystal built around a routing DSL, built-in WebSocket support and native JSON. It suits teams already writing Crystal; it does not bring an ORM, a project layout or a migration path for anyone who wants one.

**kemalcr/kemal** — Fast, Effective, Simple Web Framework

- Repository: https://github.com/kemalcr/kemal
- Website: https://kemalcr.com
- Stars: 3,919 · Forks: 206
- Language: Crystal
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/kemalcr-kemal

## The problem Kemal solves, and the developer it is aimed at

Kemal targets a specific complaint: you want the ergonomics of a Ruby micro-framework with the runtime characteristics of a compiled language. The README frames it as a table of problems and answers, and the first row is the whole pitch. "I want C-level performance with Ruby-like syntax" is answered by Crystal plus Kemal. The second row is WebSocket support with no extra shards, and the fourth is a framework that "stays out of my way", with no forced ORM and no magic.

So the audience is narrow and identifiable. It is a developer who already accepts Crystal as a language, or who is willing to learn it, and who wants an HTTP layer rather than an application skeleton. If you are looking for a framework that generates a directory tree, wires a database, and gives you a console, Kemal is not that and does not pretend to be. The philosophy section states the core is intentionally simple and that "most power comes from Crystal and middleware, not hidden magic".

The trade-off is explicit in the same paragraph. You get routing, middleware, ECR templates and static file serving in the core, while "advanced concerns" live in separate shards. Session management is the clearest example: the README lists it as a key feature but describes it as "Production-ready via kemal-session", which is a separate dependency, not part of the framework you install with the shard entry shown in the quick start.

## Routing, the handler chain, and how a request reaches your block

The mechanism is a routing DSL over Crystal's HTTP server. You require "kemal", declare routes with HTTP verb macros such as get, post and ws, and call Kemal.run at the bottom of the file. A route block either returns a value that becomes the response body, or takes an env argument and writes to env.response directly. The README's quick start shows both forms side by side: a bare get "/" returning a string, and a get "/api" that sets env.response.content_type to application/json and returns a hash converted with to_json.

Kemal also implements method negotiation more carefully than most micro-frameworks. When a path exists but the verb does not match, the README states Kemal answers 405 Method Not Allowed with an Allow header listing the methods that path accepts, citing RFC 9110 section 15.5.6. A path that is not routed at all remains a 404. HEAD is included in Allow wherever a GET route exists, because Kemal serves HEAD from the GET route.

The WebSocket case is where the design gets interesting. A ws route counts as GET, because the handshake is a GET request, but it is listed without HEAD. A plain GET on a WebSocket path, meaning a handshake missing its Upgrade header, stays a 404 rather than producing an Allow: GET that would be misleading. The README's example registers ws "/chat" alongside post "/chat", and notes that PUT /chat returns 405 with Allow: GET, POST while GET /chat returns 404.

One detail worth internalising before you write an error handler: Kemal::InitHandler presets Content-Type: text/html on every response, so a 405 handler returning JSON has to set the content type itself. The README says the Allow header is set before your handler runs, so your handler owns the body but cannot drop the header the RFC requires. The README also notes that authentication belongs in before_all or in middleware, not in individual route blocks.

## Installing Kemal and getting a JSON route running

The README's quick start assumes Crystal is already installed and uses the standard Crystal project generator. This creates a shard skeleton in a directory named my-app and moves you into it.

```bash
crystal init app my-app
cd my-app
```

Next, add Kemal to shard.yml. The README gives the dependency exactly as below, pinned to the GitHub repository rather than a version number.

```yaml
dependencies:
  kemal:
    github: kemalcr/kemal
```

Replace the generated src/my-app.cr with an application that defines a plain route and a JSON route. The second block sets the content type explicitly, which is what you want for an API endpoint since the framework presets text/html.

```crystal
require "kemal"

get "/" do
  "Hello World!"
end

get "/api" do |env|
  env.response.content_type = "application/json"
  {status: "ok"}.to_json
end

Kemal.run
```

Install the dependency and run the file. The README states the server listens on http://localhost:3000, so visiting that address after the command below should show Hello World!.

```bash
shards install
crystal run src/my-app.cr
```

If you want to try the HTTP QUERY method instead, the README documents a query macro that behaves like the other verbs, reading the payload from env.params.json or env.params.body, with before_query and after_query filters available. A QUERY request carrying a body but no Content-Type header is rejected with 400.

## Where Kemal stops: sessions, ORM and the missing project structure

The most consequential limitation is that Kemal deliberately does not own persistence or sessions. The README is direct about this: no forced ORM, and session management arrives through kemal-session. That means the framework you install from the quick start cannot answer "where do I store the session" on its own. You choose, and you own the consequences of that choice.

Project structure is the second gap. There is no generator that lays out controllers, models and configuration. The examples directory shows the intended pattern instead: examples/json-api, examples/postgresql-db, examples/mysql-db, examples/redis, examples/cookies, examples/cors, examples/file-upload, examples/file-download, examples/sse, examples/unix-domain-socket and examples/websocket-chat each demonstrate one concern in isolation. That is a useful reference set, but it is a set of starting points, not a convention you can lean on as the application grows.

The third limitation is the language itself. Kemal's type safety and native compilation come from Crystal, and so does its ceiling on contributors and libraries. The README's comparison table lists Kemal with type safety and fibers while Sinatra, Flask and Express are marked as lacking type safety, but it does not discuss ecosystem size, and the repository does not claim parity with Ruby or Python package counts. If your project depends on a specific third-party integration, check that a Crystal shard exists before you commit to the stack.

Finally, the performance numbers in the README are the project's own published figures, not independent measurements: roughly 85,000 req/sec for Hello World at 100 connections, about 50,000 for JSON serialization, about 40,000 for static files, roughly 0.5 KB memory per request and a binary around 2 MB with dependencies. Treat them as the maintainers' claims and benchmark your own workload.

## Kemal compared with Sinatra and Flask

The README's comparison table puts Kemal at roughly 85K requests per second against Sinatra at about 5K, Flask at about 3K and Express at about 15K. Those are the project's figures and the table does not state the hardware or the benchmark harness, so the ratios are more useful than the absolute numbers. The structural differences matter more than the throughput column.

Sinatra is the closest analogue in feel: a routing DSL, blocks as handlers, no imposed structure. The difference is everything underneath. Sinatra runs on the Ruby VM with threads, and its ecosystem is RubyGems. Kemal compiles to a native binary and its ecosystem is Crystal shards, which the README acknowledges by keeping advanced concerns outside the core. If your team already maintains Ruby infrastructure, Sinatra drops into it; Kemal does not, because there is no Ruby VM to share.

Flask differs in the opposite direction. It also ships a template engine and a minimal core, but it carries a larger extension ecosystem and runs on CPython. The README marks Flask as lacking built-in WebSocket support, which is one of Kemal's stated core features, and as using threads rather than fibers for concurrency. If your application is mostly synchronous request-response with a mature Python dependency graph, Flask's ecosystem is the deciding factor and Kemal's binary size is irrelevant to you.

Express is the odd one out in the table: it is marked as having native JSON handling like Kemal, but no built-in WebSockets and no single-binary deployment. Node's async model and npm's package surface are the trade you make there. Kemal's differentiator across all three rows is the combination of a compiled binary, fibers and WebSockets in the core, and the cost is that you leave three large package ecosystems behind.

## Maintenance, releases and what the MIT licence leaves you to decide

The repository is not archived, and the last push was on 2026-09-15. Releases have been frequent: v1.12.0 on 2026-07-21, v1.13.0 on 2026-08-24 and v1.14.0 on 2026-09-15, the last of these on the same day as the most recent push. That cadence is the useful signal for upgrade planning, because it means minor versions arrive on a roughly monthly rhythm and you should expect to move forward rather than sit on a version.

The README's badge states Crystal 1.19 or newer. That is the constraint to check before anything else, because a Crystal upgrade is a toolchain upgrade, not a shard bump. The repository ships a CHANGELOG.md and a CONTRIBUTING.md at the top level, so release-by-release upgrade notes exist; read them rather than assuming a minor version is behaviour-neutral, especially around the HTTP method handling described above, which is specified against RFC 9110 and RFC 10008.

Kemal is MIT licensed. In practical terms that is a permissive licence with no copyleft obligation on your application, and the repository includes a LICENSE file at the top level. This is not legal advice, and if you redistribute Kemal inside a product or vendor the source, have your own counsel read the licence text rather than a summary of it.

## Conclusion

Adopt Kemal if your team already writes Crystal or wants a single native binary with routing, WebSockets and JSON in the core, and you are willing to pick your own ORM and session store. Do not adopt it as a Ruby-to-Crystal shortcut if you need Sinatra's gem ecosystem or a framework that hands you a project layout. Before committing, verify the Crystal version your toolchain resolves against the 1.19+ badge, check that kemal-session covers your session needs, and read the 405 and QUERY behaviour in the README against your own routes.

## FAQ

### Is Kemal production ready?

The README answers yes, stating Kemal has been used in production since 2015 with 5M+ downloads. That is the project's own claim; the repository does not publish an independent audit.

### Does Kemal support WebSockets without extra dependencies?

Yes. The README lists WebSocket and real-time support as a key feature and states it is built in with no extra dependencies needed. The ws route macro handles the handshake, and the repository includes examples/websocket-chat.

### Which Crystal version does Kemal require?

The README carries a badge stating Crystal 1.19 or newer. The quick start otherwise assumes Crystal and the shards tool are already available on your machine.

### What happens in Kemal when a path exists but the HTTP method does not match?

The README states Kemal returns 405 Method Not Allowed with an Allow header listing the accepted methods, per RFC 9110 section 15.5.6, while an unrouted path returns 404. HEAD is included in Allow wherever a GET route exists.

### Does Kemal include an ORM or session storage?

No. The README says there is no forced ORM, and session management is described as production-ready via the separate kemal-session shard. The examples directory shows database integrations for PostgreSQL, MySQL and Redis as standalone examples.

## Sources

- [kemalcr/kemal on GitHub](https://github.com/kemalcr/kemal)
- [License: MIT](https://github.com/kemalcr/kemal/blob/master/LICENSE)
- [Project website](https://kemalcr.com)
- [README](https://github.com/kemalcr/kemal/blob/master/README.md)
- [Releases](https://github.com/kemalcr/kemal/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kemalcr-kemal
