Performance helpers that skip per-field GraphQL and REST serialization overhead on Django, DRF, and FastAPI/SQLAlchemy.
Project description
fastberry
Performance helpers for read-heavy Python web APIs, on both GraphQL and REST.
On large or deeply-nested payloads, the per-instance / per-field overhead of
GraphQL and REST frameworks dominates response time — pure CPU cost with no
extra database work. fastberry lets you opt specific hot paths out of that
machinery.
Two independent helpers, usable separately:
fast_path(GraphQL) — skipstrawberry-django'sdjango_resolveroverhead on hot types. Targets sync Django.fastberry.rest(REST) — read-only nested serialization that assembles the tree from column-projected queries and encodes withorjson. Works on both Django/DRF and FastAPI/SQLAlchemy — the backend is picked from the model class.
Install
The core package has no hard dependencies; pick the extra for the helper you want, and only that stack gets pulled in:
pip install 'fastberry[graphql]' # GraphQL: fast_path on strawberry-django (+ Django, strawberry)
pip install 'fastberry[rest]' # REST on Django/DRF (+ Django, orjson)
pip install 'fastberry[sqlalchemy]' # REST on FastAPI/SQLAlchemy — no Django (+ SQLAlchemy, orjson)
Requires Python 3.10+. Django (4.2+) is only installed by the graphql and
rest extras; the sqlalchemy extra is Django-free.
GraphQL: fast_path
strawberry-django wraps every field resolver in django_resolver, which calls
in_async_context() on each resolution. Under sync Django that call raises and
catches a RuntimeError internally — a fixed per-field CPU cost (roughly a
microsecond or two per field on current strawberry-django; more on older
versions) with no extra database work. Because it is paid per field, on large
or wide result sets it adds up and dominates response time.
Mark hot types with @fast_path (outermost, after @strawberry_django.type)
and register FastPathExtension on the schema:
import strawberry
import strawberry_django
from fastberry import fast_path, FastPathExtension
from myapp.models import Stock
@fast_path
@strawberry_django.type(Stock, disable_optimization=True)
class StockType:
id: int
title: str
@strawberry.type
class Query:
stocks: list[StockType] = strawberry_django.field()
schema = strawberry.Schema(query=Query, extensions=[FastPathExtension])
Only types decorated with @fast_path take the fast path; everything else
resolves normally. To turn it off, remove the extension (global) or the
decorator (per type). Needs the graphql extra
(pip install 'fastberry[graphql]').
One decorator instead of two
To avoid stacking @fast_path + @strawberry_django.type on every hot type,
use the combined wrapper. It forwards all arguments to strawberry_django and
applies fast_path on top:
from fastberry import strawberry_django as fast_strawberry_django
@fast_strawberry_django.type(Stock, disable_optimization=True)
class StockType:
id: int
title: str
fast_strawberry_django.type and .interface are exposed. You still register
FastPathExtension on the schema once. Needs the graphql extra
(pip install 'fastberry[graphql]').
Generate the GraphQL type from the model
If you don't want to hand-write the type at all, decorate the model with
fast_schema. It builds a strawberry_django type from the model's fields,
applies fast_path, and stores it on the model as __fast_type__:
from fastberry.strawberry_django import fast_schema
@fast_schema
class Stock(models.Model): ... # all concrete fields
@fast_schema(fields=["id", "title"], name="StockType")
class Other(models.Model): ... # subset + custom name
StockType = Stock.__fast_type__ # wire into your schema
Extra keyword args (e.g. disable_optimization=True) are forwarded to
strawberry_django.type. The decorator returns the model unchanged, so it
composes with other model decorators. Needs the graphql extra.
How it works: the field→resolver map is built once, at class-definition
time, keyed by the GraphQL type name (info.parent_type.name). Plain fields
resolve via a direct getattr; related managers are materialized with .all();
custom resolvers are called directly, bypassing the django_resolver wrapper.
Benchmark (the strawberry_django example;
3 200 stocks, sync Django + Postgres, query optimizer enabled on both sides so
only the per-field resolver overhead differs). The win scales with the number
of fields resolved per type — wide, hot types benefit most:
| Fields / type | Without fast-path | With fast-path | Speedup |
|---|---|---|---|
| 1 | 46.7 ms | 37.1 ms | 1.26× |
| 8 | 123.9 ms | 78.7 ms | 1.57× |
| 32 | 380.5 ms | 214.4 ms | 1.77× |
| 64 | 726.0 ms | 403.9 ms | 1.80× |
The speedup is roughly flat across dataset size (~1.5× on a 5-field type) and
grows with query depth and field count; on a wide real schema (~27 fields/type)
it reaches ~2.4×. See example/BENCHMARKS.md for the
full size/depth/field sweeps.
REST: fastberry.rest
A nested serializer (DRF's ModelSerializer, or hand-rolled object-graph
walking) builds an instance at every node of a relational tree and runs a
per-field pipeline across the whole tree. On a deep tree (3-4 relations) this
becomes the bottleneck even at a few hundred top-level objects.
fastberry.rest declares the output shape once, then at serialization time
fetches each level with a single column-projected query, assembles the tree in
Python via indexed dicts (no N+1), and encodes with orjson. It is read-only
by design — keep validation and writes on DRF or Pydantic.
It works on Django/DRF and FastAPI/SQLAlchemy as equal, first-class
backends: you write the FastRest declaration once and the backend is picked
from the model class. Needs the rest extra for Django
(pip install 'fastberry[rest]') or the sqlalchemy extra for SQLAlchemy
(pip install 'fastberry[sqlalchemy]').
Declare the shape (both backends)
from fastberry.rest import FastRest
class ProductRest(FastRest):
class Meta:
model = Product
fields = ["id", "name", "ean"]
class StockRest(FastRest):
product = ProductRest() # forward FK
class Meta:
model = Stock
fields = ["id", "title", "amount", "price"]
class SpaceRest(FastRest):
stocks = StockRest(many=True) # reverse FK
class Meta:
model = Space
fields = ["id", "name"]
class HouseRest(FastRest):
spaces = SpaceRest(many=True)
class Meta:
model = House
fields = ["id", "name", "address"]
The same four classes run unchanged whether Product/Stock/Space/House
are Django models or SQLAlchemy mapped classes — only the call that drives them
differs, as shown below.
Django / DRF
Serialize a queryset directly:
rows = HouseRest.serialize(House.objects.all()) # list[dict]
body = HouseRest.serialize_json(House.objects.all()) # bytes (orjson)
Or skip wiring a schema per view: decorate the model with @fast_rest and use
FastJSONRenderer. The view returns a raw queryset/instance; the renderer finds
the registered schema and fast-serializes it. Unregistered models fall back to
DRF's normal JSON.
from fastberry.rest import fast_rest
from fastberry.rest_renderers import FastJSONRenderer
@fast_rest(fields=["id", "title", "amount"]) # explicit field list
class Stock(models.Model): ...
@fast_rest(depth=2) # auto-derive: expand 2 levels
class House(models.Model): ... # FKs -> nested objects,
# reverse FKs -> nested lists
class HouseList(APIView):
renderer_classes = [FastJSONRenderer]
def get(self, request):
return Response(House.objects.all()) # no serializer needed
@fast_rest styles (mutually exclusive):
fields=[...](+ optionalnested={"attr": SubSchema}) — you control exactly what is emitted. Recommended when the model has sensitive columns.depth=N— auto-derive from the model's fields/relations, expandingNrelation levels (cycles broken by falling back to the FK id).depth=0emits scalars + FK ids only. Auto-derive emits every field at each expanded level — prefer explicitfieldson models with secrets.- Omitted — all concrete fields, FKs as ids, no nesting.
You can also register a hand-written nested FastRest for renderer pickup with
register_schema(Model, MyRest). Set FastJSONRenderer globally via DRF's
DEFAULT_RENDERER_CLASSES to apply it everywhere; only @fast_rest models take
the fast path, so it's safe alongside ordinary endpoints.
FastAPI / SQLAlchemy
Declare the same classes against your SQLAlchemy models. SQLAlchemy needs a
session, so pass session= to serialize*() (a select() is optional for
filtering/ordering; omit the source for all rows):
rows = HouseRest.serialize(session=session) # all rows
body = HouseRest.serialize_json(select(House).where(...), session=session)
In FastAPI, hand the bytes straight back — no DRF, no renderer:
@app.get("/houses")
def houses(session: Session = Depends(get_session)):
return Response(HouseRest.serialize_json(session=session),
media_type="application/json")
See the runnable example/fastapi project. (This works only
with an ORM fastberry.rest understands — Django or SQLAlchemy — not ORM-less
stacks or other ORMs.)
Benchmark
Measured on the django example; the SQLAlchemy backend runs
the same column-projected, one-query-per-level fetch. 200 houses × 4 spaces × 8
stocks = 6400 leaves, 4 levels deep, plus an FK to product; full tree queryset →
JSON bytes, Postgres:
| Approach | min ms | queries | vs DRF+prefetch |
|---|---|---|---|
| DRF nested, no prefetch | 1891 | 7401 | 0.05× |
| DRF nested, with prefetch | 91.6 | 4 | 1.0× |
| fastberry.rest FastRest | 15.9 | 4 | 5.8× |
The advantage grows with payload size — ~3.4× at 300 leaves up to ~6.9× at
32 000 — while the query count stays flat at 4. See
example/BENCHMARKS.md for the full sweep.
Supported relations in this version: forward foreign key (single) and reverse
foreign key / one-to-many (many=True), on both the Django and SQLAlchemy
backends. ManyToMany is not yet handled.
Caveats
@fast_pathtargets sync Django specifically — async resolution doesn't pay thedjango_resolveroverhead it removes.fastberry.resthas no such restriction; it works on Django/DRF and FastAPI/SQLAlchemy alike.- Both helpers are read-optimized.
@fast_pathskips custom resolver logic;fastberry.restdoes no validation. Use them on read-heavy paths; keep complex/write logic on the default framework path.
Links
- Examples:
example/— runnable Strawberry, strawberry-django, Django/DRF, and FastAPI/SQLAlchemy projects (each with adocker-compose.yml), plusexample/BENCHMARKS.md - Source: https://github.com/davidsarosi92/fastberry
- Issues: https://github.com/davidsarosi92/fastberry/issues
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastberry-0.3.1.tar.gz.
File metadata
- Download URL: fastberry-0.3.1.tar.gz
- Upload date:
- Size: 26.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9122a0419b867408914cd3135785568660ca51cf02bde322d4aea5fe409c71ac
|
|
| MD5 |
1b0daf2cb733c13e5b008b8f7f09631a
|
|
| BLAKE2b-256 |
5b9f408cca958c75ad343c49cdeb6ccc5b5a077bb835bbac4be73e40c1154e61
|
Provenance
The following attestation bundles were made for fastberry-0.3.1.tar.gz:
Publisher:
release.yml on davidsarosi92/fastberry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastberry-0.3.1.tar.gz -
Subject digest:
9122a0419b867408914cd3135785568660ca51cf02bde322d4aea5fe409c71ac - Sigstore transparency entry: 1682411623
- Sigstore integration time:
-
Permalink:
davidsarosi92/fastberry@be3c089b72101ccb0927062ee55c92ae128d2e5b -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/davidsarosi92
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@be3c089b72101ccb0927062ee55c92ae128d2e5b -
Trigger Event:
push
-
Statement type:
File details
Details for the file fastberry-0.3.1-py3-none-any.whl.
File metadata
- Download URL: fastberry-0.3.1-py3-none-any.whl
- Upload date:
- Size: 22.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab11f749a4579d7fd7053ea581e68f1c7db02b4ee67ae9422e859926d39e5000
|
|
| MD5 |
e47bffeca97a9b225e5a8e29436a2981
|
|
| BLAKE2b-256 |
614093044f891fa0ce8553188bb0f3f64dfe01c0d60695dddb9e02ca95d221d1
|
Provenance
The following attestation bundles were made for fastberry-0.3.1-py3-none-any.whl:
Publisher:
release.yml on davidsarosi92/fastberry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastberry-0.3.1-py3-none-any.whl -
Subject digest:
ab11f749a4579d7fd7053ea581e68f1c7db02b4ee67ae9422e859926d39e5000 - Sigstore transparency entry: 1682411733
- Sigstore integration time:
-
Permalink:
davidsarosi92/fastberry@be3c089b72101ccb0927062ee55c92ae128d2e5b -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/davidsarosi92
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@be3c089b72101ccb0927062ee55c92ae128d2e5b -
Trigger Event:
push
-
Statement type: