Docs / Use Cases / Throttling Simulation

Throttling Simulation

Probabilistically inject the throttle error code that each AWS service actually returns: DynamoDB ProvisionedThroughputExceededException, S3 SlowDown, SQS OverLimit, Lambda TooManyRequestsException, and so on. Tune the rate globally or per service, then drive load and watch your retry / backoff logic earn its keep.

What the demo does. Start LocalEmu with SIMULATE_THROTTLING=1 THROTTLE_RATE=0.3, fire 20 parallel requests at each of DynamoDB, S3 and SQS, and observe the per-service real error code at roughly the configured rate. Then switch to the Python SDK with retries={max_attempts: 5, mode: adaptive} and watch every call still land. For the full retry-budget drill (50 invocations, 10-way concurrent, zero-loss assertion at the end), see the Chaos Resilience walkthrough.

Real Error Codes

Each service returns the exact same throttling error code that real AWS uses. Not a generic 429.

🚦

Configurable Rate

Set global or per-service throttle rates. Test at 5%, 30%, or 80% to stress-test your retry logic.

🔄

Test Retries

Verify that SDK adaptive retry, exponential backoff, and custom retry logic all work before production.

Step-by-Step Walkthrough

Step 1: Enable throttling simulation

Terminal
$ SIMULATE_THROTTLING=1 THROTTLE_RATE=0.3 localemu start

THROTTLE_RATE=0.3 means 30% of requests get throttled. Default is 0.05 (5%). Set SIMULATE_THROTTLING=1 to enable the feature.

Step 2: DynamoDB throttling

Terminal
$ for i in $(seq 1 20); do
    awsemu dynamodb put-item \
      --table-name test-table \
      --item "{\"id\": {\"S\": \"item-$i\"}}" &
  done; wait

# Server log (illustrative: roughly 6 of 20 requests throttled at rate=0.3).
# Throttling is probabilistic, the exact mix varies run to run.
AWS dynamodb.PutItem => 200
AWS dynamodb.PutItem => 200
AWS dynamodb.PutItem => 400 (ProvisionedThroughputExceededException)
AWS dynamodb.PutItem => 200
AWS dynamodb.PutItem => 400 (ProvisionedThroughputExceededException)
AWS dynamodb.PutItem => 200
AWS dynamodb.PutItem => 200
AWS dynamodb.PutItem => 400 (ProvisionedThroughputExceededException)
...
AWS dynamodb.PutItem => 200
AWS dynamodb.PutItem => 200

Send 20 rapid PutItem requests. The server log shows ProvisionedThroughputExceededException (400) at approximately the configured rate (binomial variance around n * rate). The AWS CLI has built-in retry, so most requests eventually succeed despite throttling.

Step 3: S3 throttling

Terminal
$ for i in $(seq 1 20); do
    awsemu s3api put-object \
      --bucket test-bucket \
      --key "file-$i.txt" \
      --body /dev/null &
  done; wait

# Server log:
AWS s3.PutObject => 200
AWS s3.PutObject => 503 (SlowDown)
AWS s3.PutObject => 200
AWS s3.PutObject => 200
AWS s3.PutObject => 503 (SlowDown)
AWS s3.PutObject => 200
AWS s3.PutObject => 503 (SlowDown)
AWS s3.PutObject => 200
AWS s3.PutObject => 200
AWS s3.PutObject => 200

S3 returns SlowDown (503), the exact same error code real AWS uses when you exceed S3 request rate limits.

Step 4: SQS throttling

Terminal
$ for i in $(seq 1 20); do
    awsemu sqs send-message \
      --queue-url http://localhost:4566/000000000000/test-queue \
      --message-body "msg-$i" &
  done; wait

# Server log (illustrative at rate=0.3):
AWS sqs.SendMessage => 200
AWS sqs.SendMessage => 403 (OverLimit)
AWS sqs.SendMessage => 200
AWS sqs.SendMessage => 403 (OverLimit)
AWS sqs.SendMessage => 403 (OverLimit)
AWS sqs.SendMessage => 200
...
AWS sqs.SendMessage => 200

SQS returns OverLimit (403). Unlike DynamoDB, the SQS CLI does not retry on OverLimit by default, so throttled requests fail visibly. Expect around THROTTLE_RATE * n failures; the exact count is a probabilistic draw.

Step 5: SDK retry handles it automatically

Terminal
$ python3 -c "
import boto3
from botocore.config import Config

client = boto3.client(
    'dynamodb',
    endpoint_url='http://localhost:4566',
    region_name='us-east-1',
    config=Config(retries={'max_attempts': 5, 'mode': 'adaptive'})
)

succeeded = 0
for i in range(10):
    try:
        client.put_item(
            TableName='test-table',
            Item={'id': {'S': f'sdk-item-{i}'}}
        )
        succeeded += 1
    except Exception as e:
        print(f'Failed: {e}')

print(f'Result: {succeeded}/10 succeeded')
"

# With rate=0.3 and max_attempts=5, per-request success probability
# is 1 - 0.3^5 = 99.76%. Expect 10/10 almost every run.
Result: 10/10 succeeded

Python boto3 with Config(retries={'max_attempts': 5, 'mode': 'adaptive'}): all 10/10 succeeded. The SDK retried throttled requests automatically. This is the whole point: test that your retry logic works BEFORE production.

Step 6: Per-service rate overrides

Terminal
$ SIMULATE_THROTTLING=1 \
  DYNAMODB_THROTTLE_RATE=0.8 \
  S3_THROTTLE_RATE=0.5 \
  THROTTLE_RATE=0.1 \
  localemu start

# DynamoDB: 80% of requests throttled
# S3: 50% of requests throttled
# All other services: 10% (global default)

DYNAMODB_THROTTLE_RATE=0.8 throttles 80% of DynamoDB while others keep the global rate. S3_THROTTLE_RATE=0.5 for S3-specific testing. Mix and match per service.

How It Works

ThrottlingHandler in request chain

Sits after request parsing, before service dispatch. Probabilistic: random.random() < rate triggers throttling.

Per-service error codes

Each service returns its real AWS error code. DynamoDB: 400, S3: 503, SQS: 403, Lambda: 429.

Skips internal calls

Cross-service calls (e.g. Lambda invoking S3 internally) are never throttled. Only external client requests pay the dice roll.

Off by default

The handler short-circuits when SIMULATE_THROTTLING is unset. Calm-mode runs see no rejected requests at all.

Supported Error Codes

Service
Error Code
HTTP Status
DynamoDB
ProvisionedThroughputExceededException
400
S3
SlowDown
503
SQS
OverLimit
403
Lambda
TooManyRequestsException
429
SNS
Throttled
429
Kinesis
LimitExceededException
400
EC2
RequestLimitExceeded
503
Others
Throttling
400