125 Performance Tips for gqlgen in Production
125 Performance Tips for gqlgen in Production
Many of us know and love the speed and the schema-first style of gqlgen , one of the most popular GraphQL servers in the Go ecosystem. But once our application starts being used by thousands, or even millions, of users in production, performance often becomes an expensive bottleneck. In this article, I’ll gather 125 performance tips — from the fundamentals all the way to micro-optimizations — for those of you who are serious about running gqlgen in production, complete with code, data, and flowcharts.
gqlgen Optimization Roadmap
Before we dive into the 125 tips, let’s take a look at the general gqlgen optimization roadmap.
graph TD
subgraph Fundamental
A[Schema Design]
B[Efficient Go Code]
C[Proper Database Usage]
end
subgraph Intermediate
D[Batching & Caching]
E[Middleware/Instrumentation]
F[Concurrency]
end
subgraph Advanced
G[Field-level Optimizations]
H[Custom Extensions/Plugins]
I[Profiling/Benchmarks]
end
A --> D
D --> G
B --> E
C --> F
E --> H
F --> I
I. Schema and Modularization Basics (1–20)
Design a Minimal Schema
Only expose the fields and types the client actually needs.Use Scalar Types
Avoid “meh” or overly custom types; Go’s default scalars (int, float, string, boolean) map to the backend more efficiently.Restructure Nested Objects
Avoid objects nested too deeply, and group fields that are frequently used together.Document Your Schema
New developers won’t have to guess, which reduces technical debt.
Schema Example (Efficient vs. Bloated)
1# Efficient
2type User {
3 id: ID!
4 name: String!
5 email: String!
6 posts: [Post!]!
7}
8
9# Too Bloated
10type User {
11 id: ID!
12 name: String!
13 email: String
14 mobilePhone: String
15 address: String
16 twitter: String
17 facebook: String
18 linkedin: String
19 # ...and so on
20}- Group Queries/Mutations by Module
Separate user queries, transaction queries, and so on into distinct files/modules.
…
- Use schema stitching ONLY when necessary
Schema stitching is very powerful but also expensive in terms of performance.
II. Resolver Optimization (21–55)
- Avoid N+1
Usedataloaderfor batched data fetching.
1postsLoader := dataloader.NewBatchedLoader(fetchPostsBatch)Return Only the Fields You Need
GraphQL queries let the client pick fields, so make sure resolvers only fetch the requested fields.Optimize the Resolver Chain
Don’t do heavy work inside the resolver chain; delegate it to another layer.Use Context Wisely
Pass context only when it’s actually needed; never carrycontext.Background()by default.Resolve in Parallel
Resolvers with IO-heavy operations (HTTP, DB) can run concurrently (using goroutines & channels).
1go func() {
2 // long running
3 result := fetchFromRemote(ctx)
4 ch <- result
5}()
6select {
7 case res := <-ch:
8 // use the result
9 case <-ctx.Done():
10 // timeout / cancel
11}- Use an ErrorList Instead of Panic
gqlgen will return clean errors, allowing for partial success.
…
- Limit Query/Mutation Size
Use limit & offset, and optionally pagination.
…
- Cache Partial Fields in the Resolver
For heavy fields, use a cache (e.g., redis) keyed by the subquery.
III. Database Layer Optimization (56–80)
Use a Query Builder
Check out thesquirrelorsqlcpackages for safe and optimal queries.Limit SQL Fields
Select only the columns you actually need.Use the Right Indexes
Identify your most heavily used filter/lookup fields.Bulk Insert/Update
When a resolver inserts data in bulk/arrays, do a bulk operation instead of insert-in-a-loop.
…
- Short-Lived SQL Connections & Pool
Set max idle/active connections no higher than the maximum number of resolver goroutines.
…
- Optimize DB Projection Based on Query Fields
Build a dynamic SELECT based on the fields in the GraphQL selection set.
…
- Prepared Statements
Minimize SQL parsing overhead: use prepared/parameterized queries.
IV. Concurrency & Parallelism (81–95)
Resolver Pooling
Implement a worker-pool pattern if you have heavy resolvers.Maximum Worker Limit
Make sure the number of workers is proportional to your CPU capacity, and avoid deadlocks.
A Simple Worker Pattern
1func worker(jobs <-chan Job, results chan<- Result, db *sql.DB) {
2 for job := range jobs {
3 res, err := processJob(job, db)
4 results <- Result{res, err}
5 }
6}…
- Set Connection Timeouts
Cap the time spent on the backend/database to free up resources.
…
- Monitor Goroutine Leaks
Use pprof to profile goroutine usage on every deploy.
V. Caching & Batching External Calls (96–115)
Dataloader Pattern
Use thegithub.com/graph-gophers/dataloaderlibrary to batch fields.Per-Request Caching
Cache queries within the scope of a single request, especially repeated lookups.
…
- Distributed Cache
For heavy data, use Redis/Memcached.
…
- Cache Invalidation Policy
Implement an invalidation strategy on updates.
…
- Cache Field-level Partial Results
For heavy fields, key them on the input and the selection set.
VI. Instrumentation & Observability (116–125)
- Metrics on Resolvers
Track the duration of each resolver, e.g., via Prometheus:
1prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
2 //store timing
3})).ObserveDuration()Implement Context-aware Logging
Logs must carry the query id/request id.Distributed Tracing
Use OpenTelemetry to trace requests across microservices.PPROF, Trace Routines
pprof is integrated at the/debug/pprof/endpoint.
…
Load Test Pre-Production
Benchmark worst-case query scenarios.Audit the Schema and Deprecated Fields
Remove fields that are no longer in use, every quarter.
Simulation: The Effect of Caching & Dataloader Optimization
Here’s a simple simulation of the impact of batching resolvers with dataloader:
Without Dataloader (N+1 Problem)
| User | posts count | DB queries |
|---|---|---|
| u1 | 3 | 4 |
| u2 | 5 | 6 |
| u3 | 1 | 2 |
| Total | 12 |
With Dataloader (Batching)
| User | posts count | DB queries |
|---|---|---|
| u1 | 3 | 1 |
| u2 | 5 | 1 |
| u3 | 1 | 1 |
| Total | 3 |
Example Dataloader Caching Setup
1type loaders struct {
2 UserLoader *dataloader.Loader
3 PostLoader *dataloader.Loader
4}
5
6func DataloaderMiddleware(l *loaders) func(http.Handler) http.Handler {
7 //inject the loader into the request ctx
8}Conclusion
Optimizing gqlgen is not a one-off checklist. It’s an iterative marathon that must be accompanied by continuous monitoring, benchmarking, and review as your application grows. With these 125 tips, I hope you can pick the right best practices for your production context — from schema, batching, and concurrency to observability — to build a GraphQL API that is fast, scalable, and maintainable.
Have other tips or experiences? Share them in the comments!