Imagine you're building a GraphQL API that returns a list of products where each product requires data from multiple services or expensive calculations. Without response streaming, your users wait until all products are fully processed. With the @stream directive, products are sent to the client as soon as they're ready, improving perceived performance.
Until recently, GraphQL response streaming with AWS Lambda was only possible using Lambda Function URLs. But AWS now supports response streaming with Amazon API Gateway, and graphql-yoga has added support for this feature. This opens up new possibilities for building responsive GraphQL APIs with the full feature set of API Gateway (custom domains, usage plans, API keys, etc.).
What's new?
AWS announced support for response streaming in API Gateway in their blog post Building responsive APIs with Amazon API Gateway response streaming.
Instead of buffering the entire Lambda response, API Gateway now streams chunks of data directly to the client as they become available. This not only improves perceived performance but also increases the response payload limit from 6 MB to 20 MB. Combined with GraphQL's @stream directive, you can send data incrementally to your users.
My code examples are written in TypeScript. I use the AWS Cloud Development Kit (CDK), which allows you to define your cloud infrastructure as code in any of the supported programming languages.
GraphQL Server
To enable response streaming with graphql-yoga, you need three key changes:
1. Add the defer-stream plugin
import { useDeferStream } from "@graphql-yoga/plugin-defer-stream";
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createSchema, createYoga } from "graphql-yoga";
import { pipeline } from "stream/promises";
const yoga = createYoga<{
event: APIGatewayProxyEvent;
lambdaContext: Context;
res: awslambda.ResponseStream;
}>({
plugins: [useDeferStream()],
schema: createSchema({
typeDefs: /* GraphQL */ `
type Product {
id: ID!
name: String!
price: Float!
}
type Query {
products(limit: Int = 100): [Product!]!
}
`,
resolvers: {
Query: {
// ...
}
},
}),
});
The @graphql-yoga/plugin-defer-stream plugin enables the @stream and @defer directives in your schema.
2. Use async generators in your resolvers
{
Query: {
products: async function* (_parent, args) {
// Simulate fetching products from a database
for (let i = 1; i <= args.limit; i++) {
// Simulate processing delay
await new Promise((resolve) => setTimeout(resolve, 100));
yield {
id: i.toString(),
name: `Product ${i}`,
price: Math.random() * 100,
};
}
},
},
}
The async function* with yield allows graphql-yoga to stream each product as soon as it's ready.
3. Wrap your handler with streamifyResponse
import type { APIGatewayProxyEvent } from "aws-lambda";
import { pipeline } from "stream/promises";
export const handler = awslambda.streamifyResponse(async function handler(
event: APIGatewayProxyEvent,
res,
lambdaContext,
) {
const queryString = event.queryStringParameters
? "?" +
new URLSearchParams(
event.queryStringParameters as Record<string, string>,
).toString()
: "";
const response = await yoga.fetch(
`https://${event.requestContext.domainName}${event.path}${queryString}`,
{
method: event.httpMethod,
headers: event.headers as HeadersInit,
body:
event.body && event.isBase64Encoded
? Buffer.from(event.body, "base64")
: event.body,
},
{
event,
lambdaContext,
res,
},
);
res = awslambda.HttpResponseStream.from(res, {
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries()),
});
if (response.body) {
await pipeline(response.body, res);
}
res.end();
});
awslambda.streamifyResponse enables Lambda's response streaming mode, and pipeline streams the yoga response body to the Lambda response stream.
Using @stream
Now you can use the @stream directive in your queries:
query {
products(limit: 20) @stream
}
The response arrives in multipart/mixed format with incremental data:
--graphql
Content-Type: application/json
{"data":{"products":[]},"hasNext":true}
--graphql
Content-Type: application/json
{"incremental":[{"items":[{"id":"1","name":"Product 1","price":42.5}],"path":["products",0]}],"hasNext":true}
--graphql
Content-Type: application/json
{"incremental":[{"items":[{"id":"2","name":"Product 2","price":73.2}],"path":["products",1]}],"hasNext":true}
--graphql--
Each product is sent as soon as it's ready.
API Gateway
The API Gateway setup requires one important configuration: responseTransferMode: aws_apigateway.ResponseTransferMode.STREAM.
import {
aws_apigateway,
aws_lambda,
aws_lambda_nodejs,
Duration,
Stack,
type StackProps,
} from "aws-cdk-lib";
import type { Construct } from "constructs";
import * as path from "node:path";
export class GraphqlStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const graphqlLambda = new aws_lambda_nodejs.NodejsFunction(
this,
"GraphQLLambdaFunction",
{
entry: path.join(__dirname, "../lambda/graphql.ts"),
architecture: aws_lambda.Architecture.ARM_64,
runtime: aws_lambda.Runtime.NODEJS_24_X,
timeout: Duration.seconds(20),
memorySize: 256,
bundling: { minify: true },
},
);
new aws_apigateway.LambdaRestApi(this, "GraphQLLambdaRestApi", {
handler: graphqlLambda,
integrationOptions: {
responseTransferMode: aws_apigateway.ResponseTransferMode.STREAM,
},
});
}
}
One thing to watch out for: Make sure your Lambda timeout is sufficient for long-running queries, and remember that you can't change the status code after streaming has started.
Ready!
Response streaming with the @stream directive is a game-changer for GraphQL APIs that require expensive calculations or data from multiple sources. The combination of Amazon API Gateway, AWS Lambda, and graphql-yoga makes it straightforward to implement.
You can find more information in the graphql-yoga documentation, the GraphQL Defer and Stream Directives RFC, and the Amazon API Gateway streaming documentation.
If you have any kind of feedback, suggestions or ideas - feel free to comment this post!
Top comments (0)