GraphQL promised to fix overfetching, eliminate versioning headaches, and give frontend teams exactly the data they need. After years of shipping GraphQL in production for clients across SaaS, marketplaces, and mobile apps, we can confirm it delivers on those promises, but only under the right conditions. Under the wrong ones, it creates performance traps, security blind spots, and complexity that a well designed REST API would have avoided entirely.
We have written a detailed comparison of REST vs GraphQL that covers the surface level differences. This post goes deeper into what actually happens when you run GraphQL at scale and when you should walk away from it.
Where GraphQL Genuinely Wins
Multi client applications are where GraphQL earns its keep. If you are building a web dashboard, a mobile app, and a partner API from the same backend, GraphQL eliminates the need to maintain three different endpoint structures. The mobile app can request a slim payload with just the fields it needs. The dashboard can pull deeply nested data in a single round trip. The partner API gets a self documenting schema they can explore in a playground.
We saw this play out clearly on a project where the mobile client needed 4 fields from a user profile while the admin dashboard needed 28. With REST, you either overfetch on mobile or maintain two endpoints. With GraphQL, both clients send exactly the query they need and the server resolves it from the same schema.
Rapid frontend iteration is the second clear win. When a frontend team can modify their data requirements without waiting on a backend team to ship a new endpoint, development velocity increases measurably. On projects we have shipped through our full stack development practice, teams using GraphQL typically cut the time between design handoff and working UI by 30 to 40 percent because the data layer is self service.
Schema as documentation is underappreciated. A well typed GraphQL schema with descriptions is living documentation. New engineers onboard faster, API consumers discover capabilities without reading docs, and breaking changes are caught at build time with codegen tools like GraphQL Code Generator.
Where GraphQL Hurts in Production
The N+1 problem is real and pervasive. The moment you have nested resolvers, a single GraphQL query can trigger dozens or hundreds of database calls. A query for 50 users with their orders and each order is items will fire 50 order queries plus potentially 200 item queries unless you implement DataLoader or equivalent batching. In our experience, roughly 70 percent of GraphQL performance issues we have debugged trace back to unbatched resolvers.
The fix is not complicated, DataLoader solves it well, but the fact that naive GraphQL implementations silently degrade is dangerous. REST endpoints are explicitly defined, so performance characteristics are predictable. GraphQL performance depends entirely on what the client asks for, which means your worst case query might not show up until a customer with 10,000 records hits your API.
Query complexity attacks are a security concern unique to GraphQL. Because clients define query shape, a malicious actor can craft deeply nested queries that consume enormous server resources. Imagine a query that traverses user, orders, items, reviews, reviewer profiles, their orders, and so on 15 levels deep. Without query depth limiting and cost analysis, a single request can bring down your server.
Every production GraphQL deployment needs:
- Query depth limits (we typically cap at 7 to 10 levels)
- Query cost analysis that assigns weights to fields and rejects expensive queries
- Timeout enforcement at the resolver level, not just the HTTP level
- Persisted queries for public APIs, which lock down exactly which queries clients can run
Caching is harder than with REST. REST APIs cache naturally because each URL is a unique resource. GraphQL sends POST requests to a single endpoint with varying query bodies, which means HTTP caching layers like CDNs and browser caches do not work out of the box. You need application level caching with tools like Apollo Server cache hints, response cache plugins, or a dedicated caching layer like Stellate.
On one project, switching from GraphQL to REST for a high traffic public listing page dropped response times from 180ms to 12ms because the REST endpoint could be cached at the CDN edge. That same data served through GraphQL required a full server round trip every time.
The Hybrid Approach We Recommend
Most production systems we architect end up with a hybrid approach. GraphQL serves the authenticated application where clients have varying data needs and the query surface is controlled. REST serves the public facing, high traffic endpoints where caching and simplicity matter.
This is not a compromise. It is an honest assessment of where each tool excels. As we covered in our API design best practices guide, the best API strategy starts with understanding your access patterns, not picking a technology and forcing everything through it.
GraphQL is Wrong for These Cases
Simple CRUD applications with one or two clients do not need GraphQL. If your web app is the only consumer of your API and you control both sides, REST with TypeScript shared types gives you the same type safety with less infrastructure. The overhead of a schema, resolvers, and DataLoader setup is not justified.
High throughput, low latency APIs like payment processing, real time data feeds, or IoT ingestion should not use GraphQL. The query parsing, validation, and resolver execution add latency that a direct REST or gRPC endpoint avoids. When you need consistent sub 10ms responses, GraphQL is the wrong tool.
Teams without GraphQL experience will underperform with it initially. The learning curve for production grade GraphQL is steeper than REST. Schema design, resolver patterns, batching, error handling, and security all require knowledge that takes months to build. If your team is moving fast on a deadline, ship REST and migrate later if the multi client case emerges.
The Implementation Stack That Works
When GraphQL is the right call, here is the stack we reach for:
- Schema definition: Code first with Pothos or Nexus, not schema first SDL. Code first catches errors at compile time and integrates with your TypeScript types.
- Server: Apollo Server or GraphQL Yoga. Both are mature. Yoga has a lighter footprint and better integration with edge runtimes.
- Batching: DataLoader on every resolver that touches a database. No exceptions.
- Codegen: GraphQL Code Generator for typed client queries. This eliminates an entire class of runtime errors.
- Monitoring: Track resolver execution time individually, not just total request time. A slow resolver hidden inside a fast query will only show up under load.
Subscriptions: Proceed with Caution
GraphQL subscriptions for real time data sound elegant. In practice, they are one of the most operationally complex features to run at scale. WebSocket connection management, subscription lifecycle, and server memory consumption scale linearly with connected clients. At 10,000 concurrent subscriptions, you need dedicated infrastructure.
For most real time use cases we have seen, purpose built real time systems like Supabase Realtime or dedicated WebSocket servers outperform GraphQL subscriptions. Use subscriptions for prototyping. Use dedicated real time infrastructure for production.
The Decision Framework
Choose GraphQL when you have three or more distinct clients consuming the same data, when frontend teams need to iterate on data requirements independently, and when you have engineers experienced in production GraphQL patterns.
Choose REST when you have one or two clients, when caching at the HTTP layer matters, when latency is critical, or when your team is shipping against a tight deadline.
Choose both when your system is large enough that different parts have different access patterns. Most serious applications end up here.
If you are weighing GraphQL for a new project or considering a migration from REST, reach out to us. We will give you an honest assessment based on your specific architecture, team, and timeline.