AWS Developer Tools Blog
Announcing response streaming for .NET on AWS Lambda
Today, we are announcing support for AWS Lambda response streaming for .NET Lambda functions. With response streaming, functions can be more responsive by sending data back to the caller incrementally as it becomes available, rather than buffering the entire response in memory before returning it.
Why response streaming?
With the traditional Lambda invocation model, the function computes the entire response, serializes it, and returns it all at once. For workloads that produce large responses or generate output over time, this means the caller waits for the full response before seeing any data.
Response streaming changes this. Your function can start sending bytes to the caller immediately. This is useful for:
- LLM/AI responses — stream tokens from a model as they are generated
- Large data exports — send CSV or JSON lines without buffering the full dataset
- Larger responses — streaming raises the max response size from 6 MB to 200 MB (or 10 MB when used with Amazon API Gateway).
Getting started with response streaming
Response streaming is available for .NET 8 and later. To use it, your Lambda function calls LambdaResponseStreamFactory.CreateStream() to get a writable .NET System.IO.Stream. All output is written to this stream. If an invocation of the Lambda function creates a response stream, the handler’s return value is ignored.
Here is a simple example that streams “Hello” messages:
using Amazon.Lambda.Core;
using Amazon.Lambda.Core.ResponseStreaming;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace StreamingHello;
public class Function
{
public async Task FunctionHandler(string input, ILambdaContext context)
{
using var responseStream = LambdaResponseStreamFactory.CreateStream();
using var writer = new StreamWriter(responseStream);
for (var i = 1; i <= 100; i++)
{
await writer.WriteLineAsync($"Hello {input} - {i}");
if (i % 10 == 0)
{
await writer.FlushAsync();
}
}
}
}
To invoke the function, use the Lambda InvokeWithResponseStream API from the AWSSDK.Lambda package. The following example shows how to call this API and handle the streamed response:
var client = new AmazonLambdaClient();
var request = new InvokeWithResponseStreamRequest
{
FunctionName = "StreamingHello",
Payload = new MemoryStream(UTF8Encoding.UTF8.GetBytes("\"World\""))
};
var response = await client.InvokeWithResponseStreamAsync(request);
await foreach (var streamEvent in response.EventStream)
{
switch (streamEvent)
{
case InvokeResponseStreamUpdate payloadChunk:
{
var text = Encoding.UTF8.GetString(payloadChunk.Payload.ToArray());
Console.Write(text);
break;
}
case InvokeWithResponseStreamCompleteEvent complete:
{
if (complete.ErrorCode != null)
{
Console.WriteLine($"Error Code: {complete.ErrorCode}");
}
if (complete.ErrorDetails != null)
{
Console.WriteLine($"Error Details: {complete.ErrorDetails}");
}
if (complete.LogResult != null)
{
var logs = Encoding.UTF8.GetString(
Convert.FromBase64String(complete.LogResult));
Console.WriteLine("Logs:");
Console.WriteLine(logs);
}
break;
}
}
}
You can also test the Lambda function using the .NET tool Amazon.Lambda.Tools with the invoke-function command and the new --invoke-mode Stream switch. The following command demonstrates invoking the StreamingHello function and receiving its streamed output:
> dotnet lambda invoke-function StreamingHello --invoke-mode Stream --payload "World"
Amazon Lambda Tools for .NET Core applications (7.0.0)
Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli, https://github.com/aws/aws-lambda-dotnet
Payload:
Hello World - 1
Hello World - 2
Hello World - 3
Hello World - 4
...
Hello World - 97
Hello World - 98
Hello World - 99
Hello World - 100
Log Tail:
START RequestId: 3f162d6c-62b0-4e01-a041-9a850b6bd35f Version: $LATEST
END RequestId: 3f162d6c-62b0-4e01-a041-9a850b6bd35f
REPORT RequestId: 3f162d6c-62b0-4e01-a041-9a850b6bd35f Duration: 33.50 ms Billed Duration: 34 ms Memory Size: 512 MB Max Memory Used: 79 MB
Streaming an LLM response with Microsoft.Extensions.AI
A natural fit for response streaming is AI workloads where a model generates tokens over time. Using the Microsoft.Extensions.AI framework with the AWSSDK.Extensions.Bedrock.MEAI provider, you can stream an Amazon Bedrock model’s response directly to the caller. The following example demonstrates how to do this:
public class Function
{
IChatClient _chatClient;
public Function()
{
var bedrockClient = new AmazonBedrockRuntimeClient();
_chatClient = bedrockClient.AsIChatClient(Environment.GetEnvironmentVariable("MODEL_ID"));
}
public async Task FunctionHandler(string prompt, ILambdaContext context)
{
using var responseStream = LambdaResponseStreamFactory.CreateStream();
using var writer = new StreamWriter(responseStream);
await foreach (var update in _chatClient.GetStreamingResponseAsync(prompt))
{
if (update.Text is { Length: > 0 })
{
await writer.WriteAsync(update.Text);
await writer.FlushAsync();
}
}
}
}
The caller starts receiving the model’s response as soon as the first tokens are generated. For conversational AI use cases, this makes the experience feel much more responsive.
Response streaming with API Gateway REST API
Lambda response streaming works with the API Gateway REST API. When streaming through API Gateway, the response needs a prelude containing the HTTP status code and headers. Use LambdaResponseStreamFactory.CreateHttpStream to create a stream with a prelude.
The following example downloads an object from Amazon S3 and streams it directly to the caller through API Gateway without buffering the entire object in memory:
public async Task Get(APIGatewayProxyRequest request, ILambdaContext context)
{
string? key;
if (!request.PathParameters.TryGetValue("s3Key", out key))
{
var prelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude
{
StatusCode = HttpStatusCode.BadRequest,
Headers = new Dictionary<string, string>
{
["Content-Type"] = "text/plain",
}
};
using var responseStream = LambdaResponseStreamFactory.CreateHttpStream(prelude);
using var writer = new StreamWriter(responseStream);
await writer.WriteAsync("Missing 's3Key' path parameter");
}
else
{
try
{
var getResponse = await _s3Client.GetObjectAsync(_bucketName, key);
var prelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude
{
StatusCode = HttpStatusCode.OK,
Headers = new Dictionary<string, string>
{
["Content-Type"] = getResponse.Headers.ContentType,
["Content-Disposition"] = $"attachment; filename=\"{key}\""
}
};
using var responseStream = LambdaResponseStreamFactory.CreateHttpStream(prelude);
await getResponse.ResponseStream.CopyToAsync(responseStream);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
var prelude = new HttpResponseStreamPrelude
{
StatusCode = HttpStatusCode.NotFound,
Headers = new Dictionary<string, string>
{
["Content-Type"] = "text/plain"
}
};
using var responseStream = LambdaResponseStreamFactory.CreateHttpStream(prelude);
using var writer = new StreamWriter(responseStream);
await writer.WriteAsync($"Object '{key}' not found");
}
}
}
As the S3 GetObject response is read, the bytes flow directly into the Lambda response stream and out to the caller. This avoids loading the full object into memory, which is particularly useful for files up to the 10 MB API Gateway limit.
The API Gateway integration needs to be configured for streaming. In your AWS CloudFormation or AWS SAM template, the integration URI must use the /response-streaming-invocations path and set responseTransferMode to STREAM. The following template snippet shows the required integration configuration:
"DefinitionBody": {
"openapi": "3.0.1",
"info": { "title": "My Streaming API", "version": "1.0" },
"paths": {
"/{proxy+}": {
"x-amazon-apigateway-any-method": {
"x-amazon-apigateway-integration": {
"type": "aws_proxy",
"httpMethod": "POST",
"payloadFormatVersion": "1.0",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${StreamingFunction.Arn}/response-streaming-invocations"
},
"responseTransferMode": "STREAM",
"timeoutInMillis": 29000
}
}
}
}
}
Response streaming with ASP.NET Core
If you are using the Amazon.Lambda.AspNetCoreServer.Hosting package to run ASP.NET Core on Lambda, you can enable response streaming by setting EnableResponseStreaming to true in the hosting options. The following example shows how to configure and use response streaming in an ASP.NET Core Lambda app
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi, options =>
{
options.EnableResponseStreaming = true;
});
var app = builder.Build();
app.MapGet("/", () => "Hello from streaming Lambda");
app.MapGet("/stream", async (HttpContext context) =>
{
context.Response.ContentType = "text/plain";
var stream = context.Response.BodyWriter.AsStream();
using var writer = new StreamWriter(stream, leaveOpen: true);
for (var i = 1; i <= 100; i++)
{
await writer.WriteLineAsync($"Line {i}");
if (i % 10 == 0)
{
await writer.FlushAsync();
}
}
});
app.Run();
With EnableResponseStreaming set to true, the hosting layer builds the HTTP prelude from the ASP.NET Core response (status code, headers, cookies) and streams the body through the Lambda response stream. Standard ASP.NET Core endpoint returns like Results.Json(...) and Results.Text(...) work without changes.
The API Gateway integration needs to be configured for streaming. In your CloudFormation or SAM template, the integration URI must use the /response-streaming-invocations path and set responseTransferMode to STREAM. The following SAM template demonstrates the full configuration for an ASP.NET Core streaming function:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
"StreamingApi": {
"Type": "AWS::Serverless::Api",
"Properties": {
"StageName": "prod",
"DefinitionBody": {
"openapi": "3.0.1",
"info": { "title": "ASP.NET Core Streaming", "version": "1.0" },
"paths": {
"/": {
"x-amazon-apigateway-any-method": {
"x-amazon-apigateway-integration": {
"type": "aws_proxy",
"httpMethod": "POST",
"payloadFormatVersion": "1.0",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${AspNetCoreFunction.Arn}/response-streaming-invocations"
},
"responseTransferMode": "STREAM",
"timeoutInMillis": 29000
}
}
},
"/{proxy+}": {
"x-amazon-apigateway-any-method": {
"x-amazon-apigateway-integration": {
"type": "aws_proxy",
"httpMethod": "POST",
"payloadFormatVersion": "1.0",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${AspNetCoreFunction.Arn}/response-streaming-invocations"
},
"responseTransferMode": "STREAM",
"timeoutInMillis": 29000
}
}
}
}
}
}
},
"AspNetCoreFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "MyStreamingApp",
"Runtime": "dotnet10",
"CodeUri": "",
"MemorySize": 512,
"Timeout": 30,
"Policies": ["AWSLambda_FullAccess"]
}
},
"ApiPermission": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": { "Ref": "AspNetCoreFunction" },
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${StreamingApi}/*/*/*"
}
}
}
},
"Outputs": {
"ApiURL": {
"Value": {
"Fn::Sub": "https://${StreamingApi}.execute-api.${AWS::Region}.amazonaws.com/prod/"
}
}
}
}
Conclusion
Response streaming opens up new patterns for .NET Lambda functions, particularly for AI workloads and large data transfers. To get started, update to the latest versions of Amazon.Lambda.Core and Amazon.Lambda.RuntimeSupport.
For feedback on .NET Lambda response streaming, open a GitHub issue or discussion on our aws/aws-lambda-dotnet repository.