.NET Servless API on AWS Lambda

Build and deploy a C# servless API as a Dockerized AWS Lambda with API Gateway integration, scaling, and zero server management.

Building and deploying a high-performance, maintainable API can be a complex task, especially when you need to manage infrastructure, handle scaling, and ensure robust security. In this guide, I’ll show a modern approach to running a .NET serverless API on an AWS Lambda function. In this post, you’ll learn how to scaffold a minimal .NET project, implement your business logic in a clean, testable handler, and bundle it all into a Docker image that runs on Lambda’s highly scalable platform.

Why Choose a Serverless API on AWS Lambda

Here are some motivations to build such an API to run on Lambdas, at least the ones I think are more important.

Operational Simplicity

With AWS Lambda, you offload server management entirely, there are no EC2 instances to patch, no operating systems to update, and no capacity planning to worry about. AWS handles the underlying infrastructure, so you can focus on writing your code.

Automatic Scaling

Lambda automatically scales your API in response to incoming traffic: when requests surge, AWS transparently spins up more function instances; when traffic drops, it scales back down to zero. This elasticity ensures consistent performance under varying loads without manual intervention.

Cost Efficiency

You pay only for the Lambda running time you use. Lambda’s billing model is metered in 100 milliseconds, meaning idle periods don’t cost you money. For APIs with intermittent or bursty traffic patterns, this “pay-as-you-go” pricing can deliver significant savings compared to EC2 servers.

Fast Deployment and Iteration

Container images give you full control over your runtime environment: bring in custom libraries, frameworks, or native dependencies, and be confident that what runs locally is identical to what runs in production. Pushing a new Docker image to ECR and updating your Lambda function can be automated in CI/CD pipelines for fast deployment of releases.

Deep AWS Ecosystem Integration

Lambda functions integrate natively with other AWS services, such as Amazon DynamoDB, S3, SNS, and so on, enabling you to build powerful, event-driven architectures with minimal glue code. API Gateway further enhances this by providing caching (when needed), request validation, throttling, and WAF protection right out of the box.

Prerequisites

You need an AWS account with CLI credentials set (aws configure).

Install .NET 8 and the Lambda .NET Global Tool.

Install Docker to build and test your container locally.

Creating the Lambda Project

I’ll build the code example over the lambda templates provided as a template package. You can install it with this command:

dotnet new install Amazon.Lambda.Templates

This adds the AWS Lambda project templates to your .NET CLI

Use the following command to list the installed templates:

dotnet new list

This should print a bunch of templates like this:

I’ll use the template called: “Lambda ASP.NET Core Web API (.NET 8 Container Image)” or “serverless.image.AspNetCoreWebAPI”.

Scaffold a Container-ready API

Now let’s create the template project running this command:

dotnet new serverless.image.AspNetCoreWebAPI --name "NetCoreLambdaAPI" --region us-east-1

This creates a NetCoreLambdaAPI/src/NetCoreLambdaAPI directory containing the source code.

Implement Your API

Now you can implement your .NET API as you normally do. You don’t need to make special code to run as AWS Lambda.

I’ll use the default template code and change small bits as an example.

using Microsoft.AspNetCore.Mvc;

namespace NetCoreLambdaAPI.Controllers;

[Route("api/[controller]")]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [HttpGet("test")]
    public string Test(string input = "no input")
    {
        return input.ToUpper();
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody]string value)
    {
    }

    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }
}

Containerize the Function

I don’t like the default Dockerfile provided with the template, so I changed it a little bit to turn it into this:

FROM public.ecr.aws/lambda/dotnet:8 AS base

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY [".", "NetCoreLambdaAPI/"]
WORKDIR "/src/NetCoreLambdaAPI"
RUN dotnet build "NetCoreLambdaAPI.csproj" --configuration Release --output /app/build

FROM build AS publish
RUN dotnet publish "NetCoreLambdaAPI.csproj" \
--configuration Release \
--runtime linux-x64 \
--self-contained false \
--output /app/publish \
-p:PublishReadyToRun=true  

FROM base AS final
WORKDIR /var/task
COPY --from=publish /app/publish .

CMD [ "NetCoreLambdaAPI::NetCoreLambdaAPI.LambdaEntryPoint::FunctionHandlerAsync" ]

Want to know more about Docker? Check these posts: Deploying a Docker Service Stack and Set up Datalust Seq in a Docker Container

This Dockerfile uses the official .NET SDK Microsoft builder image (mcr.microsoft.com/dotnet/sdk:8.0) to build the API but it creates a running image with the official optimized AWS Lambda image (public.ecr.aws/lambda/dotnet:8).

Another important part of this Dockerfile is the last line, the CMD section. This string should point to the special method used to load the correct Lambda handler. My CMD points to: NetCoreLambdaAPI::NetCoreLambdaAPI.LambdaEntryPoint::FunctionHandlerAsync

This path can be read as: ProjectName::MainWorkspaceName.LambdaEntryPoint::FunctionHandlerAsync. The LambdaEntryPoint class is provided by the template and FunctionHandlerAsync is the name of the entry method provided by the super class.

Build and Test Locally

To build and run your code, type the following commands in the terminal:

docker build -t net-core-lambda-api:dev -f Dockerfile .
docker run -it --rm -p 9000:8080 net-core-lambda-api:dev

These commands should build your image and TAG it as net-core-lambda-api:dev and run a container exposing the port 9000 (TCP).

When the container runs, it expects some input, and the terminal should show some output like:

29 May 2025 17:59:29,934 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)

To test your API, just run:

curl -X POST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{
  "resource": "/{proxy+}",
  "path": "/api/values/test",
  "httpMethod": "GET",
  "queryStringParameters": {
    "input":"this is a simple test input"
  }
}'

The test URL seems weird because we did not write any code to generate this URL, nor this kind of input body, but remember that Lambda runs in a different environment. The AWS Lambda environment expects this body to proxy the request to your API.

This request should return something like this:

{
   "statusCode":200,
   "headers":{
      
   },
   "multiValueHeaders":{
      "Content-Type":[
         "text/plain; charset=utf-8"
      ]
   },
   "body":"THIS IS A SIMPLE TEST INPUT",
   "isBase64Encoded":false
}

The response indicates that the API is working as expected.

It’s also possible to run the API locally without using the Lambda entry point. You just need to do the usual way: running dotnet run

Push the Image to Amazon ECR

Create ECR Repository

aws ecr create-repository --repository-name net-core-lambda-api

Ensure your IAM user/role has ecr:CreateRepository and ecr:PutImage

Authenticate Docker to ECR

aws ecr get-login-password | docker login --username AWS --password-stdin <aws_account_id>.dkr.ecr.us-east-1.amazonaws.com

This logs Docker into your private ECR registry

Tag and Push

docker tag net-core-lambda-api:dev <aws_account_id>.dkr.ecr.us-east-1.amazonaws.com/net-core-lambda-api:dev
docker push <aws_account_id>.dkr.ecr.us-east-1.amazonaws.com/net-core-lambda-api:dev

The API image will now be available for Lambda deployment.

IAM Permissions

Execution Role for Lambda

Create an IAM role lambda-exec-role with this trust policy for the Lambda service. Then, attach a policy allowing:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "*"
    }
  ]
}

This grants CloudWatch Logs and ECR pull permissions to your function.

API Gateway Invoke Permission

API Gateway must be allowed to invoke your Lambda. After creating the function, run:

aws lambda add-permission \
  --function-name NetCoreLambdaApi \
  --statement-id apigateway-invoke \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn arn:aws:execute-api:us-east-1:<aws_account_id>:<api_id>/*/*/*

This manually grants apigateway.amazonaws.com the right to invoke your function. You could also do the same thing using the AWS Lambda panel.

Create the Lambda Function

aws lambda create-function \
  --function-name NetCoreLambdaApi \
  --package-type Image \
  --code ImageUri=<aws_account_id>.dkr.ecr.us-east-1.amazonaws.com/net-core-lambda-api:dev \
  --role arn:aws:iam::<aws_account_id>:role/lambda-exec-role

This spins up a Lambda function using your container image.

Configure API Gateway

Create an HTTP API

aws apigatewayv2 create-api \
  --name "NetCoreLambdaApi" \
  --protocol-type HTTP \
  --target arn:aws:lambda:us-east-1:<aws_account_id>:function:NetCoreLambdaApi

This creates an HTTP API that forwards all requests to your Lambda.

Deploy a Stage

aws apigatewayv2 create-stage \
  --api-id <api_id> \
  --stage-name prod \
  --auto-deploy

Your API is now live at https://.execute-api.us-east-1.amazonaws.com/prod

Test the Endpoint

Send a POST to your API:

curl -X GET https://<api_id>.execute-api.us-east-1.amazonaws.com/prod/api/values/test?input=hello \
     -H "Content-Type: application/json"

You should get a response code OK (200) with the word HELLO.

Conclusion

In this guide, I’ve shown how to build and pack a C# API as a containerized AWS Lambda function using the .NET Lambda image template and Docker to ensure a consistent runtime environment across development and production. The API Gateway integration exposes your Lambda via a managed HTTP endpoint with built-in throttling, caching, and request validation, protecting your backend and improving performance.

See ya!