Skip to content

Commit c591145

Browse files
fix: add transparent rate limiting to prevent AWS API throttling (#240)
Adds a token-bucket rate limiter (golang.org/x/time/rate) at 10 requests/second to proactively pace AWS API calls. This prevents TooManyRequestsException errors when processing environments with many Lambda functions, where tight sequential loops of ListVersionsByFunction, ListAliases, and DeleteFunction calls exceed the 15 rps per-account API limit. The rate limiting is transparent — no user configuration required. The limiter is applied before each AWS API call in: - getAllLambdaVersion (ListVersionsByFunction and ListAliases pagination) - getAllLambdas with custom list (GetFunction per Lambda) - deleteLambdaVersion (DeleteFunction per version) Made-with: Cursor
1 parent eb115f0 commit c591145

File tree

5 files changed

+120
-194
lines changed

5 files changed

+120
-194
lines changed

cmd/clean.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ import (
2626
internal "github.com/karl-cardenas-coding/go-lambda-cleanup/v2/internal"
2727
log "github.com/sirupsen/logrus"
2828
"github.com/spf13/cobra"
29+
"golang.org/x/time/rate"
2930
)
3031

3132
const (
3233
// Per AWS API Valid Range: Minimum value of 1. Maximum value of 10000.
3334
maxItems int32 = 10000
3435
regionFile string = "aws-regions.txt"
36+
// AWS Lambda API rate limit is ~15 requests/second per account per region.
37+
// Default to 10 rps to leave headroom for other callers in the same account.
38+
defaultAPIRPS rate.Limit = 10
3539
)
3640

3741
var (
@@ -143,7 +147,10 @@ var cleanCmd = &cobra.Command{
143147
o.APIOptions = append(o.APIOptions, middleware.AddUserAgentKeyValue("go-lambda-cleanup", VersionString))
144148
})
145149

146-
err = executeClean(ctx, &config, initSvc, customeDeleteList)
150+
limiter := rate.NewLimiter(defaultAPIRPS, 1)
151+
log.Debugf("API rate limiting: %.0f requests/second", float64(defaultAPIRPS))
152+
153+
err = executeClean(ctx, &config, initSvc, customeDeleteList, limiter)
147154
if err != nil {
148155
return err
149156
}
@@ -157,7 +164,7 @@ executeClean is the main function that executes the clean-up process
157164
It takes a context, a pointer to a cliConfig struct, a pointer to a lambda client, and a list of custom lambdas to delete
158165
An error is returned if the function fails to execute.
159166
*/
160-
func executeClean(ctx context.Context, config *cliConfig, svc *lambda.Client, customList []string) error {
167+
func executeClean(ctx context.Context, config *cliConfig, svc *lambda.Client, customList []string, limiter *rate.Limiter) error {
161168
startTime := time.Now()
162169

163170
var (
@@ -170,7 +177,7 @@ func executeClean(ctx context.Context, config *cliConfig, svc *lambda.Client, cu
170177

171178
log.Info("Scanning AWS environment in " + *config.RegionFlag)
172179

173-
lambdaList, err := getAllLambdas(ctx, svc, customList)
180+
lambdaList, err := getAllLambdas(ctx, svc, customList, limiter)
174181
if err != nil {
175182
log.Error("ERROR: ", err)
176183
log.Fatal("ERROR: Failed to retrieve Lambda list.")
@@ -184,7 +191,7 @@ func executeClean(ctx context.Context, config *cliConfig, svc *lambda.Client, cu
184191
for _, lambda := range lambdaList {
185192
lambdaItem := lambda
186193

187-
lambdaVersionsList, err := getAllLambdaVersion(ctx, svc, lambdaItem, *config)
194+
lambdaVersionsList, err := getAllLambdaVersion(ctx, svc, lambdaItem, *config, limiter)
188195
if err != nil {
189196
log.Error("ERROR: ", err)
190197
log.Fatal("ERROR: Failed to retrieve Lambda version list.")
@@ -241,14 +248,14 @@ func executeClean(ctx context.Context, config *cliConfig, svc *lambda.Client, cu
241248
return returnError
242249
}
243250

244-
err = deleteLambdaVersion(ctx, svc, globalLambdaDeleteInputStructs...)
251+
err = deleteLambdaVersion(ctx, svc, limiter, globalLambdaDeleteInputStructs...)
245252
if err != nil {
246253
log.Error("ERROR: ", err)
247254
log.Fatal("ERROR: Failed to delete Lambda versions.")
248255
}
249256

250257
// Recalculate storage size
251-
updatedLambdaList, err := getAllLambdas(ctx, svc, customList)
258+
updatedLambdaList, err := getAllLambdas(ctx, svc, customList, limiter)
252259
if err != nil {
253260
log.Error("ERROR: ", err)
254261
log.Fatal("ERROR: Failed to retrieve Lambda list.")
@@ -257,7 +264,7 @@ func executeClean(ctx context.Context, config *cliConfig, svc *lambda.Client, cu
257264
log.Info("............")
258265

259266
for _, lambda := range updatedLambdaList {
260-
updatededlambdaVersionsList, err := getAllLambdaVersion(ctx, svc, lambda, *config)
267+
updatededlambdaVersionsList, err := getAllLambdaVersion(ctx, svc, lambda, *config, limiter)
261268
if err != nil {
262269
log.Error("ERROR: ", err)
263270
log.Fatal("ERROR: Failed to retrieve Lambda version list.")
@@ -388,7 +395,7 @@ func countDeleteVersions(deleteList [][]lambda.DeleteFunctionInput) int {
388395
// deleteLambdaVersion takes a list of lambda.DeleteFunctionInput and deletes all the versions in the list
389396
// The function takes a context, a pointer to a lambda client, and a list of lambda.DeleteFunctionInput. A variadic operator is used to allow the user to pass in multiple lists of lambda.DeleteFunctionInput
390397
// Use this function with caution as it will delete all the versions in the list.
391-
func deleteLambdaVersion(ctx context.Context, svc *lambda.Client, deleteList ...[]lambda.DeleteFunctionInput) error {
398+
func deleteLambdaVersion(ctx context.Context, svc *lambda.Client, limiter *rate.Limiter, deleteList ...[]lambda.DeleteFunctionInput) error {
392399
var (
393400
returnError error
394401
wg sync.WaitGroup
@@ -400,6 +407,11 @@ func deleteLambdaVersion(ctx context.Context, svc *lambda.Client, deleteList ...
400407
func() {
401408
defer wg.Done()
402409

410+
if err := limiter.Wait(ctx); err != nil {
411+
returnError = fmt.Errorf("rate limiter interrupted: %w", err)
412+
return
413+
}
414+
403415
_, err := svc.DeleteFunction(ctx, &version)
404416
if err != nil {
405417
err = errors.New("Failed to delete version " + *version.Qualifier + " of " + *version.FunctionName + ". \n Additional details: " + err.Error())
@@ -436,7 +448,7 @@ func getLambdasToDeleteList(list []types.FunctionConfiguration, retainCount int8
436448
}
437449

438450
// getAllLambdas returns a list of all available lambdas in the AWS environment. The function takes a context, a pointer to a lambda client, and a list of custom lambdas function names to delete.
439-
func getAllLambdas(ctx context.Context, svc *lambda.Client, customList []string) ([]types.FunctionConfiguration, error) {
451+
func getAllLambdas(ctx context.Context, svc *lambda.Client, customList []string, limiter *rate.Limiter) ([]types.FunctionConfiguration, error) {
440452
var (
441453
lambdasListOutput []types.FunctionConfiguration
442454
returnError error
@@ -463,6 +475,10 @@ func getAllLambdas(ctx context.Context, svc *lambda.Client, customList []string)
463475

464476
if len(customList) > 0 {
465477
for _, item := range customList {
478+
if err := limiter.Wait(ctx); err != nil {
479+
return lambdasListOutput, fmt.Errorf("rate limiter interrupted: %w", err)
480+
}
481+
466482
input := &lambda.GetFunctionInput{
467483
FunctionName: aws.String(item),
468484
}
@@ -495,6 +511,7 @@ func getAllLambdaVersion(
495511
svc *lambda.Client,
496512
item types.FunctionConfiguration,
497513
flags cliConfig,
514+
limiter *rate.Limiter,
498515
) ([]types.FunctionConfiguration, error) {
499516
var (
500517
lambdasLisOutput []types.FunctionConfiguration
@@ -509,6 +526,10 @@ func getAllLambdaVersion(
509526

510527
p := lambda.NewListVersionsByFunctionPaginator(svc, input)
511528
for p.HasMorePages() {
529+
if err := limiter.Wait(ctx); err != nil {
530+
return lambdasLisOutput, fmt.Errorf("rate limiter interrupted: %w", err)
531+
}
532+
512533
page, err := p.NextPage(ctx)
513534
if err != nil {
514535
log.Error(err)
@@ -529,6 +550,10 @@ func getAllLambdaVersion(
529550
var aliasesOut []types.AliasConfiguration
530551

531552
for pg.HasMorePages() {
553+
if err := limiter.Wait(ctx); err != nil {
554+
return lambdasLisOutput, fmt.Errorf("rate limiter interrupted: %w", err)
555+
}
556+
532557
page, err := pg.NextPage(ctx)
533558
if err != nil {
534559
log.Error(err)

0 commit comments

Comments
 (0)