Your hottest reads don’t need GraphQL.
They don’t need a cache tier either.
BifrostQL serves the same SQL tables through more than one door. GraphQL is the door for shaped application queries: joins, pagination, aggregates. For key-addressed point reads and sparse writes, there is a second door that speaks the Redis protocol — and on those operations it is 3–5x cheaper per call, because it skips GraphQL parsing entirely and goes straight from key to SQL.
127.0.0.1:6379> GET users:42"{\"id\":42,\"name\":\"User 42\",\"email\":\"user42@example.com\",\"status\":\"active\",\"balance\":147.0}"
127.0.0.1:6379> MGET users:7 users:8 users:9 # one SQL round trip127.0.0.1:6379> GET order_items:1042:3 # composite primary key127.0.0.1:6379> HGET users:42 email # one visible fieldThe key is the address: table:pk, or table:pk1:pk2 for composite keys. Any Redis client works — redis-cli, StackExchange.Redis, ioredis — because the wire is real RESP2/RESP3.
The numbers
Section titled “The numbers”Same seeded SQLite database, same machine, every lane over a real loopback socket. Full harness in the repo at benchmarks/BifrostQL.Benchmarks.
| Operation | RESP door | GraphQL door |
|---|---|---|
| Single row by primary key | 333 μs | 1,343 μs |
| 20 rows, one round trip | 653 μs (~33 μs/row) | 2,012 μs |
| 10 composite-key rows, one round trip | 654 μs | — |
| Update one column | 286 μs | 1,263 μs¹ |
| Update two fields | 272 μs | 1,255 μs¹ |
¹ The generated GraphQL update input requires every non-nullable column, so the GraphQL write carries the full row. RESP SET/HSET update only the columns you name — that is a semantic difference between the doors, not a benchmark trick.
Two honest caveats. Absolute microseconds are hardware- and database-specific; the ratios are the durable result. And this is not a claim to beat a dedicated Redis server on raw latency — a real Redis answering from RAM is faster than any door onto SQL. The claim is different: these are your live rows. There is no second copy to load, invalidate, or watch drift out of date.
Read the RESP GuideNo cache to keep honest
The classic read path is database → cache → invalidation bugs. Here the Redis protocol answers from the source table, so a read is current by construction and there is nothing to warm, expire, or reconcile.
Security on the wire
Every read runs the same transformer pipeline as GraphQL: tenant isolation, policy row scope, soft-delete. A row your identity cannot see answers exactly like a missing key, so existence never leaks.
Batches stay batches
MGET with N keys compiles to one SQL query — for single-column and composite primary keys alike. Twenty rows cost one round trip, not twenty.
Writes are a decision
SET/HSET/DEL ship off by default. Opting in routes every write through the full mutation pipeline — tenant scoping, soft-delete, audit columns, change history, field encryption — and logs a startup warning.
Closed by default
The listener binds loopback unless a deployment says otherwise, requires AUTH before any identity-bearing command, and carries connection, size, and depth caps as shipped defaults.
Both doors, one truth
Keep GraphQL for the application and add RESP where per-call cost matters: session lookups, feature flags, entity hydration in hot request paths. Same tables, same permissions, one source of truth.