Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
02 Nov 2025 · 5 min read ·Article 125 / 125
Go

125 Performance Tips for gqlgen in Production

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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.

MERMAID
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)

  1. Design a Minimal Schema
    Only expose the fields and types the client actually needs.

  2. Use Scalar Types
    Avoid “meh” or overly custom types; Go’s default scalars (int, float, string, boolean) map to the backend more efficiently.

  3. Restructure Nested Objects
    Avoid objects nested too deeply, and group fields that are frequently used together.

  4. Document Your Schema
    New developers won’t have to guess, which reduces technical debt.

Schema Example (Efficient vs. Bloated)
graphql
 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}
  1. Group Queries/Mutations by Module
    Separate user queries, transaction queries, and so on into distinct files/modules.

  1. Use schema stitching ONLY when necessary
    Schema stitching is very powerful but also expensive in terms of performance.

II. Resolver Optimization (21–55)

  1. Avoid N+1
    Use dataloader for batched data fetching.
go
1postsLoader := dataloader.NewBatchedLoader(fetchPostsBatch)
  1. Return Only the Fields You Need
    GraphQL queries let the client pick fields, so make sure resolvers only fetch the requested fields.

  2. Optimize the Resolver Chain
    Don’t do heavy work inside the resolver chain; delegate it to another layer.

  3. Use Context Wisely
    Pass context only when it’s actually needed; never carry context.Background() by default.

  4. Resolve in Parallel
    Resolvers with IO-heavy operations (HTTP, DB) can run concurrently (using goroutines & channels).

go
 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}
  1. Use an ErrorList Instead of Panic
    gqlgen will return clean errors, allowing for partial success.

  1. Limit Query/Mutation Size
    Use limit & offset, and optionally pagination.

  1. 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)

  1. Use a Query Builder
    Check out the squirrel or sqlc packages for safe and optimal queries.

  2. Limit SQL Fields
    Select only the columns you actually need.

  3. Use the Right Indexes
    Identify your most heavily used filter/lookup fields.

  4. Bulk Insert/Update
    When a resolver inserts data in bulk/arrays, do a bulk operation instead of insert-in-a-loop.

  1. Short-Lived SQL Connections & Pool
    Set max idle/active connections no higher than the maximum number of resolver goroutines.

  1. Optimize DB Projection Based on Query Fields
    Build a dynamic SELECT based on the fields in the GraphQL selection set.

  1. Prepared Statements
    Minimize SQL parsing overhead: use prepared/parameterized queries.

IV. Concurrency & Parallelism (81–95)

  1. Resolver Pooling
    Implement a worker-pool pattern if you have heavy resolvers.

  2. Maximum Worker Limit
    Make sure the number of workers is proportional to your CPU capacity, and avoid deadlocks.

A Simple Worker Pattern
go
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}

  1. Set Connection Timeouts
    Cap the time spent on the backend/database to free up resources.

  1. Monitor Goroutine Leaks
    Use pprof to profile goroutine usage on every deploy.

V. Caching & Batching External Calls (96–115)

  1. Dataloader Pattern
    Use the github.com/graph-gophers/dataloader library to batch fields.

  2. Per-Request Caching
    Cache queries within the scope of a single request, especially repeated lookups.

  1. Distributed Cache
    For heavy data, use Redis/Memcached.

  1. Cache Invalidation Policy
    Implement an invalidation strategy on updates.

  1. Cache Field-level Partial Results
    For heavy fields, key them on the input and the selection set.

VI. Instrumentation & Observability (116–125)

  1. Metrics on Resolvers
    Track the duration of each resolver, e.g., via Prometheus:
go
1prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
2    //store timing
3})).ObserveDuration()
  1. Implement Context-aware Logging
    Logs must carry the query id/request id.

  2. Distributed Tracing
    Use OpenTelemetry to trace requests across microservices.

  3. PPROF, Trace Routines
    pprof is integrated at the /debug/pprof/ endpoint.

  1. Load Test Pre-Production
    Benchmark worst-case query scenarios.

  2. 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)

Userposts countDB queries
u134
u256
u312
Total12

With Dataloader (Batching)

Userposts countDB queries
u131
u251
u311
Total3

Example Dataloader Caching Setup

go
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!

Related Articles

💬 Comments