async-graphql: A Rust GraphQL Server Library With Static and Dynamic Schemas
A GraphQL server library implemented in Rust
At a glance
- What is it?
- async-graphql builds a spec-compliant GraphQL server from Rust types, or from a schema assembled at runtime. The derive macros are the easy part; query depth limits and the MSRV are where teams get caught.
- Who is it for?
- Adopt async-graphql if your service is already Rust and you want the schema derived from typed structs rather than written as a separate SDL document, and if you can pin a toolchain at Rust 1.86.0 or newer. Do not adopt it if your team has no Rust and only needs a GraphQL layer over an existing Node or Python service, because the integration crates and the derive macros assume you are writing the resolvers in Rust.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 163 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 26, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The Problem async-graphql Solves for Rust Services
A Rust web service that wants to expose GraphQL has two jobs: parse and validate GraphQL documents, and execute resolvers written in Rust. async-graphql covers both. The library is a server implementation, not a client and not a gateway, so it sits inside your process and answers queries over a web framework you already run.
The target reader is a backend engineer who has a Rust service and wants the schema to follow the code. The README describes the project as a high-performance GraphQL server library that is fully specification compliant, and the surrounding repository backs that up with a parser crate, a value crate and a derive crate, so the schema, the execution engine and the value model are separate packages rather than one monolith.
It is a poor fit when the GraphQL layer is a thin facade over services written in other languages. The resolvers are Rust functions, and the type mapping runs through procedural macros, so there is no path where you keep your existing resolvers and only swap the transport.
Static Schema, Dynamic Schema and the Derive Macros
The static path is the one most teams want. You write a struct, annotate the impl block with the Object macro, and each async method becomes a field. The README's first example defines a Query struct with an async fn howdy returning a static string, then builds a schema with Schema::build(Query, EmptyMutation, EmptySubscription).finish(). EmptyMutation and EmptySubscription are placeholders for services that only read. The macro turns the method into a resolver, and the return type into the GraphQL type.
The dynamic path is for schemas that are not known at compile time. It requires the dynamic-schema feature, which is on by default according to the feature table, and it builds the same schema from values: Object::new("Query").field(Field::new("howdy", TypeRef::named_nn(TypeRef::STRING), |_| FieldFuture::new(async { "partner" }))). Note the two differences from the static example. The field type is written as a TypeRef rather than inferred from a Rust signature, and the builder returns a Result, so finish() is followed by a question mark. That error value is where a malformed dynamic schema surfaces.
Both paths end at the same Schema type, which is what the integration crates accept. The trade-off is real: the static schema gets compile-time checking and rustfmt-friendly macro output, while the dynamic schema trades that checking for the ability to assemble fields from configuration or from a remote source.
Installing async-graphql and Serving a First Query
The crate is published on crates.io as async-graphql, and the README points at the Cargo package page and the docs.rs page. Add it with cargo add, which also pulls the derive, parser and value crates as dependencies:
cargo add async-graphql
cargo add async-graphql-poem
cargo add poem tokio --features tokio/fullThe README's static example uses the poem integration. The handler returns GraphiQLSource::build().finish() wrapped in Html, the schema is built once at startup, and the route serves GraphiQL on GET and GraphQL on POST:
use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Object, Schema};
use async_graphql_poem::*;
use poem::{listener::TcpListener, web::Html, *};
struct Query;
#[Object]
impl Query {
async fn howdy(&self) -> &'static str {
"partner"
}
}
#[handler]
async fn graphiql() -> impl IntoResponse {
Html(GraphiQLSource::build().finish())
}A query for the howdy field returns the string partner. The README prints the GraphiQL URL as http://localhost:8000 and binds the listener to 0.0.0.0:8000, so the first thing to check after cargo run is that port and not the crate's own documentation site.
If you are on axum instead of poem, the integration crate is async-graphql-axum and it lives in the integrations directory of the same repository. The examples are not in this repository: the README says all examples are in a sub-repository and gives git submodule update followed by cd examples && cargo run --bin [name].
Limiting Query Depth and Complexity Before You Ship
The README carries a security note that recommends limiting the complexity and depth of queries in a production environment to avoid possible DDoS attacks. It names three builder methods: SchemaBuilder.limit_complexity, SchemaBuilder.limit_depth and SchemaBuilder.limit_directives. These are opt-in. Nothing in the default feature set turns them on, so a schema built exactly as the README's first example builds it has no ceiling on how deep a client can nest a query.
That is the single most important operational detail in the repository. The library gives you the mechanism, and the documentation gives you the link to the depth_and_complexity page in the book, but the default configuration is permissive. A public endpoint that copies the quickstart verbatim and stops there is exposed. The cost model behind limit_complexity is described in the book, not in the README, so that page is the one to read before the first deploy.
A second constraint sits in the package metadata. The README states a minimum supported Rust version of 1.86.0 or later, while the Cargo.toml in the repository sets rust-version to 1.89.0. Those two numbers disagree, and the manifest is the stricter of the pair. A team pinning an older toolchain should check both before assuming the README's floor applies to the release they resolve.
Integrations, File Uploads and Subscriptions
async-graphql does not ship an HTTP server. It ships a GraphQL handler that a framework routes to, and the repository keeps those bindings in an integrations directory. The README lists poem, actix-web, warp, rocket and axum, with crates for the first three and in-repository paths for rocket and axum. Picking a framework is therefore a separate decision from picking the GraphQL library, and the integration crate is the only piece that changes.
On top of the core execution there is a feature table, and most entries are off by default. The exceptions called out in the README are the GraphiQL integration and, per the Cargo.toml default list, dynamic-schema and tempfile. Everything else is opt-in: chrono, uuid, url, decimal, time and jiff for scalar integrations, apollo_tracing and apollo_persisted_queries for the Apollo extensions, dataloader for batching, and log or tracing for instrumentation.
The feature list also names capabilities that are not features in the Cargo sense but are implemented in the crate: multipart file uploads, WebSocket subscriptions, batch queries, error extensions and custom extensions. Apollo Federation v2 is listed as a feature of the library as a whole. Each of those adds surface area to your endpoint, and each is worth enabling deliberately rather than all at once.
Where async-graphql Is the Wrong Choice
The clearest case against it is a polyglot backend. If the data lives behind Python or Node services and the Rust component would only forward queries, then the resolver layer has to be rewritten in Rust for no gain, and the derive macros that make the library pleasant become work you did not need to do.
A second case is a schema that is owned outside the codebase. GraphQL APIs in larger organisations are often published as SDL and reviewed by consumers before implementation. async-graphql's static path derives the schema from Rust types, so the SDL is an output rather than the source of truth. The dynamic path can build a schema from configuration, which narrows the gap, but it gives up the compile-time checking that is the main reason to use the static path in the first place. Teams that treat the SDL as the contract should weigh that inversion carefully.
The third case is toolchain rigidity. The README's stated minimum is Rust 1.86.0, the manifest requires 1.89.0, and the package uses edition 2024. A project frozen on an older compiler cannot simply add the crate and move on.
async-graphql Against Juniper and the GraphQL-over-HTTP Route
The natural comparison in Rust is juniper, which is the other long-standing GraphQL server library in the ecosystem. The difference in approach is visible in the README's first example: async-graphql resolvers are async fns, and the library is built around async/await from the ground up, with the tokio feature and futures-util among its dependencies. A resolver that awaits a database call is the normal shape of the code. Juniper's model grew around synchronous resolvers with an explicit execution context, and its async support arrived as an addition to that model rather than as its starting point. Which one fits depends on whether your resolvers are I/O bound and how much of your code is already async.
The second alternative is not a library at all. If the schema is stable and the resolvers are simple, a GraphQL-over-HTTP layer in front of an existing service can be enough, and the Rust process never needs to know about GraphQL types. That route gives up the type safety that async-graphql's macros provide, and it gives up the depth and complexity limits that the library exposes on its builder, so the protections have to be rebuilt at the edge. The repository also ships a benches directory with a static_schema benchmark target, which is the place to look if execution overhead is the deciding factor rather than API shape.
Maintenance, Licensing and What an Upgrade Costs
The repository is not archived, and the last push was on 2026-04-21. That is roughly five months before today, so the project is not dormant, but neither is it a repository with daily activity, and no release history is available for it here. The package version in Cargo.toml is 8.0.0-rc.5, which means the current line is a release candidate rather than a stable release, and that matters for anyone who pins exact versions.
Licensing is dual: the Cargo.toml declares MIT OR Apache-2.0, and the repository carries both LICENSE-MIT and LICENSE-APACHE at the top level. The README's own framing is Apache-2.0, so the two sources describe the same arrangement differently. For a consumer this is a permissive choice either way, and the per-file terms are what a legal review would read; nothing here is legal advice.
The upgrade cost is dominated by the toolchain rather than the API. Edition 2024 and rust-version 1.89.0 in the manifest mean a version bump can force a compiler upgrade across the workspace, and the README's 1.86.0 figure will mislead anyone who reads only the README. The crate also forbids unsafe code, which removes one class of audit question but does not change the dependency graph. A CHANGELOG.md sits at the repository root, and that file, not the README, is where the breaking changes between releases are recorded.
Editorial conclusion
Adopt async-graphql if your service is already Rust and you want the schema derived from typed structs rather than written as a separate SDL document, and if you can pin a toolchain at Rust 1.86.0 or newer. Do not adopt it if your team has no Rust and only needs a GraphQL layer over an existing Node or Python service, because the integration crates and the derive macros assume you are writing the resolvers in Rust. Before committing, check the version you resolve against the 1.89.0 rust-version in the current Cargo.toml, confirm that limit_depth and limit_complexity are wired into your SchemaBuilder, and read the depth_and_complexity page in the book rather than trusting the default configuration.
Frequently asked questions
What is the purpose of async-graphql?
It is a GraphQL server library implemented in Rust. It parses and executes GraphQL documents inside your process and exposes the result through an integration crate for a web framework such as poem, axum or actix-web.
How does async-graphql compare with juniper?
async-graphql is built around async/await from the start, so resolvers are async fns and awaiting I/O inside a resolver is the normal shape. Juniper is the other long-standing Rust GraphQL server library, and its model grew around synchronous resolvers with async support added later.
Does async-graphql support a dynamic schema?
Yes. The dynamic path requires the dynamic-schema feature, which the Cargo.toml lists in the default feature set, and it builds the schema from Object and Field values instead of from Rust types. The builder's finish() returns a Result in that mode.
Which Rust version does async-graphql need?
The README states a minimum supported Rust version of 1.86.0 or later, while the Cargo.toml in the repository sets rust-version to 1.89.0 and uses edition 2024. The manifest is the stricter of the two.
How do I limit query depth in async-graphql?
The schema builder exposes limit_depth, limit_complexity and limit_directives. The README recommends using them in production to avoid possible DDoS attacks, and the book's depth_and_complexity page describes the cost model.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/async-graphql-async-graphql)