Trying Out Amazon Cognito's New Feature, the "Provisioned Limit API"
This page has been translated by machine translation. View original
Introduction
The API rate limits for Amazon Cognito User Pools are managed through a two-tier quota system. One is the limit set per AWS account and Region (Account-level max limit), and the other is the Provisioned limit, which can be adjusted within that range. The Provisioned limit can be changed immediately with the UpdateProvisionedLimit API, but raising the upper limit itself requires a service quota request.
Iwasa's article introduces the API for operating Provisioned Limits itself. As a follow-up, in this article I actually requested a service quota increase and, after approval, raised the UserCreation limit from 50 to 75 RPS. I measured the improvement in throughput with a parallel load test written in Go.
Verification
Service quota increase request
First, I checked the state before the increase. I retrieved the current value for the UserCreation category with GetProvisionedLimit.
aws cognito-idp get-provisioned-limit \
--region ap-northeast-1 \
--limit-definition '{"LimitClass":"API_CATEGORY","Attributes":{"Category":"UserCreation"}}'
Both ProvisionedLimitValue and FreeLimitValue were 50.
{
"Limit": {
"LimitDefinition": {
"LimitClass": "API_CATEGORY",
"Attributes": {
"Category": "UserCreation"
}
},
"ProvisionedLimitValue": 50,
"FreeLimitValue": 50
}
}
To change the Provisioned limit to a value greater than 50 in this state, you first need to raise the Account-level max limit via a service quota increase. I requested the UserCreation limit of 75 in Service Quotas. The quota code is L-5987B8A0 (Rate of UserCreation requests).
aws service-quotas request-service-quota-increase \
--region ap-northeast-1 \
--service-code cognito-idp \
--quota-code L-5987B8A0 \
--desired-value 75
The request was accepted with a PENDING status.
{
"RequestedQuota": {
"ServiceName": "Amazon Cognito User Pools",
"QuotaName": "Rate of UserCreation requests",
"DesiredValue": 75.0,
"Status": "PENDING",
"Created": "2026-07-03T16:54:34+09:00"
}
}
Full output of request-service-quota-increase
{
"RequestedQuota": {
"Id": "9e1e8001-example-request-id",
"ServiceCode": "cognito-idp",
"ServiceName": "Amazon Cognito User Pools",
"QuotaCode": "L-5987B8A0",
"QuotaName": "Rate of UserCreation requests",
"DesiredValue": 75.0,
"Status": "PENDING",
"Created": "2026-07-03T16:54:34+09:00",
"Requester": "{\"accountId\":\"123456789012\",\"callerArn\":\"arn:aws:sts::123456789012:assumed-role/example-role/example-user\"}",
"QuotaArn": "arn:aws:servicequotas:ap-northeast-1:123456789012:cognito-idp/L-5987B8A0",
"GlobalQuota": false,
"Unit": "None",
"QuotaRequestedAtLevel": "ACCOUNT"
}
}
This service quota increase involved correspondence with the service team via a support case, in which I was asked to explain the use case and to confirm the desired limit value, target, and period of use. It took more than 10 days, spanning two weekends.
Confirming the limit increase
After approval, I set the Provisioned limit to 75 with UpdateProvisionedLimit.
aws cognito-idp update-provisioned-limit \
--region ap-northeast-1 \
--limit-definition '{"LimitClass":"API_CATEGORY","Attributes":{"Category":"UserCreation"}}' \
--requested-limit-value 75
When I checked the state after the change with GetProvisionedLimit, ProvisionedLimitValue had been updated to 75. FreeLimitValue remained at 50, and the excess 25 RPS is subject to charges.
{
"Limit": {
"LimitDefinition": {
"LimitClass": "API_CATEGORY",
"Attributes": {
"Category": "UserCreation"
}
},
"ProvisionedLimitValue": 75,
"FreeLimitValue": 50
}
}
Load test
To measure the effect of the increase, I created a tool in Go that runs AdminCreateUser in parallel with goroutines. Each test attempts to create 1000 users. To count throttling directly, SDK retries are disabled, and the setup records the number of TooManyRequestsException occurrences.
The test environment is as follows.
| Item | Details |
|---|---|
| Instance | EC2 c8g.xlarge (Graviton4, 4 vCPU, 8 GiB) |
| OS | Amazon Linux 2023 (ARM64) |
| Language | Go 1.22 |
| SDK | AWS SDK for Go v2 |
| HTTP | SDK default |
| Concurrency control | Semaphore based on goroutines + channels |
| Retry | Disabled (retry.AddWithMaxAttempts(retry.NewStandard(), 1)) |
| Region | ap-northeast-1 (Tokyo) |
The test code is as follows.
Load test code (Go)
package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"sync"
"sync/atomic"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/retry"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
)
func main() {
concurrency := flag.Int("concurrency", 10, "Number of concurrent goroutines")
totalUsers := flag.Int("users", 1000, "Total number of users to create")
userPoolID := flag.String("user-pool-id", "", "Cognito User Pool ID")
region := flag.String("region", "ap-northeast-1", "AWS Region")
flag.Parse()
prefix := fmt.Sprintf("loadtest-%d", time.Now().Unix())
// Load AWS config with NO retries (we want to measure throttling directly)
cfg, err := config.LoadDefaultConfig(context.Background(),
config.WithRegion(*region),
config.WithRetryer(func() aws.Retryer {
return retry.AddWithMaxAttempts(retry.NewStandard(), 1)
}),
)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load AWS config: %v\n", err)
os.Exit(1)
}
client := cognitoidentityprovider.NewFromConfig(cfg)
var (
successCount int64
throttleCount int64
latencies []time.Duration
latencyMu sync.Mutex
)
sem := make(chan struct{}, *concurrency)
var wg sync.WaitGroup
startTime := time.Now()
for i := 0; i < *totalUsers; i++ {
wg.Add(1)
sem <- struct{}{}
go func(seq int) {
defer wg.Done()
defer func() { <-sem }()
username := fmt.Sprintf("%s-%04d", prefix, seq)
reqStart := time.Now()
_, createErr := client.AdminCreateUser(context.Background(),
&cognitoidentityprovider.AdminCreateUserInput{
UserPoolId: userPoolID,
Username: aws.String(username),
MessageAction: types.MessageActionTypeSuppress,
})
elapsed := time.Since(reqStart)
if createErr != nil {
var tooMany *types.TooManyRequestsException
if errors.As(createErr, &tooMany) {
atomic.AddInt64(&throttleCount, 1)
}
} else {
atomic.AddInt64(&successCount, 1)
latencyMu.Lock()
latencies = append(latencies, elapsed)
latencyMu.Unlock()
}
}(i)
}
wg.Wait()
totalDuration := time.Since(startTime)
// Calculate percentiles and output results...
}
I ran the test for each of Limit=50 and Limit=75 while varying the concurrency.
Results
The load test results are as follows. This is a simple verification with only one measurement per condition, but the effect of the increase was most noticeable in the load range where the effective RPS reaches around the default Limit.
Concurrency 10 (effective RPS about 33)
| Limit | Success | Throttled | Effective RPS | Duration | p50 (ms) | p95 (ms) | p99 (ms) |
|---|---|---|---|---|---|---|---|
| 50 | 1000 | 0 | 32.52 | 30.75s | 293.01 | 400.52 | 702.82 |
| 75 | 996 | 4 | 34.30 | 29.04s | 287.32 | 337.10 | 361.56 |
Almost no throttling occurred at either Limit=50 or 75. This is a load range where the effective RPS does not reach the default Limit. However, 4 throttles occurred in the Limit=75 run, so it is possible that a brief concentration of requests momentarily reached near the Limit.
Concurrency 20 (effective RPS about 65–70)
| Limit | Success | Throttled | Effective RPS | Duration | p50 (ms) | p95 (ms) | p99 (ms) |
|---|---|---|---|---|---|---|---|
| 50 | 689 | 311 | 64.43 | 10.69s | 287.50 | 393.02 | 473.02 |
| 75 | 1000 | 0 | 69.73 | 14.34s | 277.38 | 336.68 | 413.25 |
A clear difference appeared. With Limit=50, 311 requests were throttled, but after raising to Limit=75, there were 1000 successes and 0 throttles, completely eliminating the throttling. This is a load range where the effective RPS exceeds the default Limit of 50 and sticks to the ceiling.
Concurrency 50 (effective RPS about 137–158)
| Limit | Success | Throttled | Effective RPS | Duration | p50 (ms) | p95 (ms) | p99 (ms) |
|---|---|---|---|---|---|---|---|
| 50 | 386 | 614 | 137.13 | 2.82s | 303.49 | 455.73 | 492.47 |
| 75 | 513 | 487 | 157.95 | 3.25s | 278.83 | 354.73 | 384.73 |
Throttling occurred even at Limit=75. This is because the total attempt rate including throttled requests is 1000 / 3.25s ≒ 308 req/s, which greatly exceeds the raised Limit of 75. Even so, the effective RPS improved from about 137 to about 158, and the number of successes also increased from 386 to 513. The increase in the Limit shows up as a difference in successful throughput.
Pricing
If you set the Provisioned limit above the default free tier (50 RPS), monthly charges apply to the excess. According to Cognito's pricing page, a permanent increase costs $20/RPS/month, and a temporary increase (minimum 1 day) costs $45/RPS/month (prorated daily). Increasing by 25 RPS from 50 to 75 and keeping it permanently raised would cost 25 × $20 = $500/month. Rather than leaving it permanently raised, a practical approach is to raise and lower it with the UpdateProvisionedLimit API only when needed.
The costs incurred in this test are as follows.
| Item | Details |
|---|---|
| Increase amount | 25 RPS (50→75) |
| Extension duration | About 3 minutes 32 seconds (confirmed from the UpdateProvisionedLimit event in CloudTrail) |
| RPSAdjustment (temporary increase $45/RPS/month) | $0.05 |
| RPSFull (base $20/RPS/month) | $0.04 |
| Total | $0.09 |
In this measurement, the usage recorded in Cost Explorer (0.00199 RPS-Month) matched the value back-calculated from the CloudTrail timestamps, so the charges were prorated by the second.
Note that the users created during the test (Cognito Essentials MAU: 5,339) were within the free tier, so they cost $0.00.
Summary
In this verification, by raising the Provisioned Limit for Amazon Cognito's UserCreation from 50 to 75 RPS, I confirmed an improvement in successful throughput in the load range where Cognito-caused throttling had occurred. Provisioned Limits and service quota increases are an effective solution when Cognito's API rate limit is the bottleneck.
However, in real systems, bottlenecks may also arise elsewhere besides Cognito, such as application-side concurrency control, SDK retries, HTTP clients, the network, and downstream processing. It is important to first isolate where the system is getting stuck with a test that reproduces the peak load.
Also, raising the Account-level max limit requires a service quota request, and approval can take time. If large-scale user registration or sign-in is expected, I recommend proceeding with verification and the request on a schedule with some margin, rather than rushing to apply after discovering that Cognito is the bottleneck.
