Throttling requests in an API is a common practice for several reasons, mainly related to the following:
Performance: Throttling safeguards backend services from being overwhelmed by excessive requests. Without it, a sudden traffic surge could overload servers, causing degraded performance or crashes.
Cost Management: APIs often incur usage-based costs. Throttling helps manage expenses by limiting the number of processed requests.
Security: Throttling restricts the number of requests a single client can make within a specific period. Enforcing rate limits prevents malicious users from flooding the API with excessive requests, ensuring service continuity for other users.
User Experience: Throttling enhances the overall user experience by preventing system overload and maintaining API responsiveness. By managing the load effectively, we can provide more consistent performance for all users.
In Amazon API Gateway, throttling can be applied at multiple levels:
Account-level: Sets limits that apply to all APIs within an AWS account in a specific region. The default rate limit is 10,000 requests per second, with a default burst limit of 5,000.
Stage-level: Throttling can be applied at the stage level, affecting all requests to any method within that stage. Additionally, limits can be set at the method level within an API stage.
Usage plan-level: Usage plans allow us to define limits for individual clients. Similar to stage-level, limits can also be set at the method level.
In this post, we will focus on the usage plan level. Throttling per client can be effectively managed using these components:
API keys: Unique alphanumeric strings that identify a client of your API.
API stages: Specific stage of our API.
Usage plans: This component allows us to control access to our API stages by defining who can access them (using API keys) and how much they can use (using limits). An API key can be associated with multiple usage plans. A usage plan can be associated with multiple API stages. However, a given API key can only be linked to one usage plan per API stage.
Throttling:
Burst limits: The maximum number of requests allowed in a short period.
Rate limits: The steady-state rate of requests per second.
Quota limits: The maximum number of requests that can be made within a specified period (day, week, or month).
Don't use API keys for authentication or authorization to control access to your APIs. If you have multiple APIs in a usage plan, a user with a valid API key for one API in that usage plan can access all APIs in that usage plan.
Pre-requisites
An IAM User with programmatic access
Install the AWS CLI
Install the Amazon Lambda Templates (
dotnet new -i Amazon.Lambda.Templates
)Install the Amazon Lambda Tools (
dotnet tool install -g Amazon.Lambda.Tools
)Install AWS SAM CLI
The Lambda function
Run the following commands to set up our Lambda function:
dotnet new lambda.EmptyFunction -n MyLambda -o .
dotnet add src/MyLambda package Amazon.Lambda.APIGatewayEvents
dotnet new sln -n MyApplications
dotnet sln add --in-root src/MyLambda
Open the Program.cs
file and update the content as follows:
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Text.Json;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace MyLambda;
public class Function
{
public APIGatewayHttpApiV2ProxyResponse FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
{
context.Logger.LogInformation(JsonSerializer.Serialize(input));
return new APIGatewayHttpApiV2ProxyResponse
{
Body = @"{""Message"":""Hello World""}",
StatusCode = 200,
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};
}
}
AWS SAM template
At the solution level, create a template.yml
file with the following content:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
SAM
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
MyApiFunction:
Type: AWS::Serverless::Function
Properties:
Timeout: 60
MemorySize: 512
Tracing: Active
Runtime: dotnet8
Architectures:
- x86_64
Handler: MyLambda::MyLambda.Function::FunctionHandler
CodeUri: ./src/MyLambda/
Events:
Get:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /hello-world
Method: get
Outputs:
MyApiEndpoint:
Description: "API endpoint"
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello-world"
This file does not yet contain the usage plan configuration. Run the following commands to deploy the resources to AWS:
sam build
sam deploy --guided
Usage plan setup
Add the following resource to the template.yml
file:
MyUsagePlan:
Type: 'AWS::ApiGateway::UsagePlan'
Properties:
UsagePlanName: MyUsagePlan
ApiStages:
- ApiId: !Ref MyApi
Stage: !Ref MyApi.Stage
Throttle:
BurstLimit: 5
RateLimit: 5
Quota:
Limit: 1000
Period: DAY
The AWS::ApiGateway::UsagePlan
resource creates a usage plan and associates it with one or more API stages:
ApiStages
: The associated API stages of a usage plan.Quota
: Maximum number of requests clients can make.Throttle
: Specifies the overall request rate (average per second) and burst capacity (number of requests that can be handled concurrently) when clients call the API.
API key setup
Add the following resource to the template.yml
file:
MyApiKey:
Type: 'AWS::ApiGateway::ApiKey'
Properties:
Enabled: true
Name: MyApiKey
MyUsagePlanKey:
Type: 'AWS::ApiGateway::UsagePlanKey'
Properties:
KeyId: !Ref MyApiKey
KeyType: "API_KEY"
UsagePlanId: !Ref MyUsagePlan
The AWS::ApiGateway::ApiKey
resource creates a unique key that we can distribute to our clients, and the AWS::ApiGateway::UsagePlanKey
resource associates it with the usage plan.
API Gateway setup
Update the AWS::Serverless::Api
resource as follows:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Auth:
ApiKeyRequired: 'true'
With this configuration, all methods for the API Gateway will require an API Key during invocation. Another option could be to put the configuration at the method level like this:
MyApiFunction:
Type: AWS::Serverless::Function
Properties:
Timeout: 60
MemorySize: 512
Tracing: Active
Runtime: dotnet8
Architectures:
- x86_64
Handler: MyLambda::MyLambda.Function::FunctionHandler
CodeUri: ./src/MyLambda/
Events:
Get:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /hello-world
Method: get
Auth:
ApiKeyRequired: true
Output setup
Add the following output to the template.yml
file:
ApiKeyValue:
Description: "CLI command to get the API key value"
Value: !Sub "aws apigateway get-api-key --api-key ${MyApiKey.APIKeyId} --include-value --query \"value\" --output text"
The output above will show the command we can use to get the API Key value to call our endpoint. As a final step, redeploy the resources to AWS.
Testing
We will use Postman to test our endpoint. If we try to access the endpoint without the x-api-key
header, the result will look like this:
Run the AWS CLI command from the deployment output and use the result as the x-api-key
header.
In the current approach, we use CloudFormation resources to set up our usage plan. However, there is another way to do the same using only the AWS::Serverless::Api
resource through the property ApiUsagePlan
. Depending on your needs, this could be a shortcut for the configuration.
You can find the code and scripts here. Thank you, and happy coding.