GraphQL has emerged as a modern alternative to RESTful APIs, offering a more flexible and efficient way for clients to query data. Unlike REST, where clients often make multiple requests to different endpoints and receive fixed response structures, GraphQL allows clients to request exactly the data they need — and nothing more — in a single round trip. This reduces the issues of over-fetching and under-fetching common in REST, and gives frontend developers more control over the shape of the response.
AWS AppSync is a managed service that helps developers build scalable, real-time GraphQL APIs with minimal operational overhead. It integrates seamlessly with various AWS data sources, including DynamoDB, Lambda, RDS, and OpenSearch, and supports features such as offline access, subscriptions, and fine-grained authorization. AppSync takes care of scaling and security, allowing teams to focus on defining their data and resolvers.
In AppSync, resolvers are the core building blocks that connect GraphQL fields to data sources. Each field in a GraphQL query — including nested fields — can have its own resolver. When a query is executed, AppSync invokes these resolvers individually, mapping request and response data using Velocity templates (VTL) or direct Lambda functions. While this resolver-per-field model gives developers flexibility, it can introduce a performance challenge known as the N+1 problem when working with nested data.
In this post, we’ll explore what the N+1 problem looks like in AWS AppSync, why it becomes a bottleneck at scale, and how to architect efficient resolvers to solve it using batch resolvers and Lambda optimizations.
Understanding the N+1 Problem in AppSync
When working with GraphQL, it’s common to request nested data in a single query. AppSync supports this by allowing each field — including deeply nested ones — to have its own resolver that fetches data from a backend data source. While this design provides flexibility and modularity, it can lead to an inefficient execution pattern known as the N+1 problem.
What Is the N+1 Problem?
The N+1 problem usually occurs with list queries. When your GraphQL API ends up making one query to fetch the root items (1), and N additional queries for each nested field, where N is the number of root items returned in the list.
Let’s take an example to understand this clearly.
query {
books {
name
title
author {
firstnName
lastName
}
}
}
Here’s what typically happens behind the scenes in AppSync
- books resolver fetches a list of books — let’s say 100 (N) items.
- For each of those 100 books, the author resolver is called individually (resulting in 100 calls).
In total, that’s 1 + 100 = 101 (N+1) resolver invocations for a single client query.
If you have even more nested queries, this becomes worse. In the below query, there is an additional field (address) that requires more resolver invocations.
query {
books {
name
title
author {
firstName
lastName
address {
city
state
}
}
}
}
- books resolver fetches a list of books — let’s say 100 items.
- For each of those 100 books, the author resolver is called individually (resulting in 100 calls).
- Then for each author, the address resolver is called again (another 100 calls).
In total, that’s 1 + 100 + 100 = 301 resolver invocations for a single client query.
Why Is This a Problem?
This approach scales poorly:
- Performance degrades linearly with the number of parent items.
- It results in high latency due to the number of sequential or parallel resolver invocations.
- It increases backend load and pressure on data sources like Lambda, RDS, or DynamoDB.
- It can quickly hit throttling limits or increase costs when using Lambda or other pay-per-request services.
While this might be acceptable for small datasets or low traffic, the N+1 pattern becomes a serious performance bottleneck at scale. Imagine serving thousands of queries per second — this inefficient pattern can overwhelm backend systems, increase response times, and degrade the user experience.
Solving N+1 with Batch Resolvers in AppSync
One of the most effective ways to overcome the N+1 problem in AWS AppSync is by using batch resolvers. The idea is simple: instead of resolving nested fields one-by-one (which results in many resolver calls), we batch them together into a single call , usually handled by a Lambda function.
Let’s explore how this works and why it’s such a powerful pattern.
How Batch Resolvers Work
In AppSync, each nested field (such as author or address) can have its own resolver, which is typically invoked for each parent object.
To convert this into a batch operation:
- Instead of calling the author resolver N times (once for each book), you configure the author field to invoke a single Lambda function that accepts a list of book IDs (or author IDs).
- This Lambda function fetches all the authors in one go and returns the results mapped back to their respective books.
Think of it as “fan-in” batching: one resolver invocation processes multiple parent objects.
Let’s apply this to our previous query and see how this works.
query {
books {
name
title
author {
firstName
lastName
}
}
}
If you use a batch resolver for the author field:
- AppSync groups all books[*].author field resolvers into one Lambda call.
- You receive an array of bookIds or authorIds within the lambda function.
- The Lambda fetches and returns the authors in bulk.
With this optimization, you’ve reduced the number of records from N+1 to just 2, which is a substantial improvement. Moreover, it’s now independent of the number of records.
Here are some additional benefits of this solution:
- Fewer Resolver Invocations: Reduces hundreds of resolver calls to just one. This saves you from the lambda concurrency limit and also reduces the pressure on downstream services.
- Faster Performance: Lower network overhead and latency.
- Clean Separation: Keeps resolver responsibilities modular while still optimizing performance.
- Cost-Efficient: Fewer Lambda invocations result in reduced AWS costs.
Enabling Batch Resolvers
Batch resolvers are currently only compatible with Lambda data sources, and this feature is not enabled by default. However, enabling this is very straightforward.
- Create a Lambda datasource as usual
- Go to create a resolver and select the created Lambda datasource
- Enable batching and set the batching size
4.Update the resolver request function to use the BatchInvoke operation
export function request(ctx) {
return {
operation: 'BatchInvoke',
payload: {
ctx: ctx
},
};
}
5.Now your lambda function will receive not a single context, but an array of contexts for each listed item. You can update the lambda function logic to do a batch get and return the results. You must ensure the returned items are in the same order as the received context order.
It’s as simple as that, but it offers significant performance gains to your GraphQL service.
Conclusion
Optimizing GraphQL queries in AWS AppSync is essential when building scalable and performant APIs — especially when dealing with nested data structures. The N+1 problem, while subtle, can lead to serious performance bottlenecks if left unaddressed.
By leveraging batch resolvers, you can drastically reduce the number of resolver calls, minimize round-trips to your data source, and deliver faster, more efficient responses to your clients. Whether you choose direct Lambda resolvers or pipeline resolvers, designing with batching in mind ensures your AppSync APIs are ready to perform at scale.
As your application grows, keeping an eye on resolver patterns and query performance becomes even more important. With the right strategy, tools, and architecture in place, you can build GraphQL services that are both elegant and efficient.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.