How many writes per second before a new DynamoDB table throttles?
A new On-demand capacity DynamoDB table takes 4,000 writes per second. We offered one 8,000. It served 4,000 a second, the number AWS documents, and rejected the rest with an HTTP 400.
More capacity arrived without us asking, but late. The first step, to 5,000 a second, came 8.6 minutes into the run. If launch day needs more than 4,000 writes a second, set warm throughput before the traffic arrives.
We measured one fresh table per run in us-east-1 on August 27 and 28, 2026, with ~1 KB items and random keys, plus one September retest for the 10-second burst.
The public measurement that most write-ups cite is from 2019.
A new table holds 4,000 writes a second after a 10-second burst
Our load generator sent writes at a fixed rate for 30 seconds, then stepped the rate up. We call that rate offered. Achieved is what DynamoDB accepted, counted request by request and averaged over the window.
| Offered | Achieved (30-second average) | Throttled requests |
|---|---|---|
| 1,000/s | 1,000/s | 0 |
| 2,000/s | 2,000/s | 0 |
| 3,000/s | 3,000/s | 0 |
| 4,000/s | 4,000/s | 0 |
| 5,000/s | 4,132/s | 25,992 |
| 6,000/s | 4,131/s | 55,966 |
| 8,000/s | 4,134/s | 115,922 |
Every write up to 4,000 a second went through. Above that, the 30-second average sat at 4,131 to 4,134 whatever we offered. DynamoDB rejected 17% of the requests at 5,000 a second and 48% at 8,000.
Those averages sit above 4,000 because each step opened with a burst. A September retest with 10-second buckets showed each throttled step at 4,400 to 4,480 writes a second for its first 10 seconds, then at 4,000.
Ten seconds at 4,400 and twenty at 4,000 average to about 4,133, which matches the August windows. The rate a new table sustains is the documented 4,000.
That burst was small. Ten seconds at 400 to 480 above 4,000 adds up to 4,000 to 4,800 extra write units.
Before its 5,000 step, our August table ran 120 seconds at 1,000 to 4,000 writes a second. That left at least 180,000 units unused below 4,000. Our runners also paused 2 seconds between windows.
AWS documents burst capacity, which keeps up to 300 seconds of unused capacity for spikes. The burst we saw was far smaller than that allows. We do not know why.
The throttle is an HTTP 400 that suggests a hot key we did not have
When DynamoDB rejects a write at the ceiling, the error reads:
ThrottlingException: Throughput exceeds the current capacity of your table or index. DynamoDB is automatically scaling your table or index so please try again shortly. If exceptions persist, check if you have a hot key: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.htmlThe status code is 400, not a 5xx. A client that retries only 5xx responses drops these writes. An alarm that watches only 5xx stays green while half the writes fail.
The first rejection came 3.8 seconds into the 5,000 window, 1.9 seconds into the 6,000 window and 0.9 seconds into the 8,000 window.
Our keys were random UUIDs, so no single key was hot. AWS now attaches a ThrottlingReasons field to throttling errors, which names the limit that fired. We did not record it.
For an on-demand table with no maximum set, AWS lists two reasons: a per-partition limit and an account-level quota per table. Our 4,000 sits far below that quota's default of 40,000, so the per-partition limit most likely fired.
We timed only the requests that DynamoDB accepted. In the 8,000 window, their median stayed at 4 to 5 ms on seven of eight runners. The eighth hit its cap on requests in flight, and its median rose to 78 ms, its p99 to 1,327 ms.
Extra capacity arrived after 8.6 minutes and stopped short of 8,000
The AWS on-demand page says a new table is "able to sustain up to 4,000 writes per second and 12,000 reads per second."
The same page says on-demand "instantly accommodates up to double the previous peak traffic on a table." For growth past double, it warns that "throttling can occur if you exceed double your previous peak within 30 minutes."
To watch the growth, we held a separate fresh table at 8,000 writes a second of offered load. Each runner Lambda had a 10-minute timeout, so the load ran as four 8-minute waves, with pauses of 32 to 39 seconds between them.
We counted accepted writes in 10-second buckets. Minutes count from the start of the first wave:
| Wave | Minutes | Served per second |
|---|---|---|
| 1 | 0–8.0 | ~4,000 |
| 2–3 | 8.6–24.2 | ~5,000 |
| 3 | 24.2–25.2 | ~6,000 |
| 4 | 25.8–33.8 | ~7,000 |
The table served 4,000 a second for the whole first wave. It reached 5,000 at the start of wave 2, 6,000 near the end of wave 3 and 7,000 at the start of wave 4. It never served the 8,000 we offered.
Our first wave went from no traffic to 8,000 at once. AWS advises pre-warming or spreading growth over at least 30 minutes, and it warns of throttling otherwise. The first wave fits that warning.
The doubling clause predicts a faster climb after that. After eight minutes at 4,000, our 8,000 was exactly double that peak, not more. After about 15 minutes at 5,000, double was 10,000, more than we sent.
Yet every 10-second bucket of all four waves had throttles. AWS does not define how long traffic must last to count as a previous peak. So we cannot say why the doubling clause did not hold.
Two of the three steps came at a wave start, right after an idle pause. The first three waves also opened with a burst bucket: 4,223, 5,293 and 5,293 writes a second. We cannot tell whether time under load or the pauses moved the table.
This is one run, and we have not shown that its shape repeats. The 2019 test ended with its table at about 7,000 writes a second under 9,000 offered. Ours ended at 7,000 under 8,000.
To take more than 4,000 writes on day one, set warm throughput first
AWS defines warm throughput as the reads and writes a table "can instantaneously support". A new on-demand table reports 4,000 write units and 12,000 read units a second.
You can raise those values before an event, which AWS calls pre-warming. The table keeps its billing mode. A raised value cannot be lowered again.
# Raise an existing table's write warm throughput to 10,000/s
aws dynamodb update-table --table-name Orders \
--warm-throughput WriteUnitsPerSecond=10000
# Status reads UPDATING, then ACTIVE when the new value is set
aws dynamodb describe-table --table-name Orders \
--query 'Table.WarmThroughput'create-table accepts the same --warm-throughput flag, so a new table can start warm. The update runs in the background, and AWS says its duration depends on the values and the table size. Start it well before the event.
Each global secondary index has its own warm throughput. If an index cannot absorb its updates, DynamoDB throttles writes to the base table. So warm the indexes too:
aws dynamodb update-table --table-name Orders \
--global-secondary-index-updates \
'[{"Update":{"IndexName":"ByCustomer","WarmThroughput":{"WriteUnitsPerSecond":10000}}}]'Pre-warming has a one-time charge: one write capacity unit at the regional hourly rate for each unit of increase. In us-east-1, raising writes from 4,000 to 10,000 costs 6,000 × $0.00065, or $3.90.
Maximum throughput is a different on-demand setting. AWS throttles requests above it on a best-effort basis, and burst capacity can briefly exceed it. It does not pre-warm a table.
Warm throughput counts write units. One unit covers a write of up to 1 KB, so a 3 KB item takes three. One partition key takes at most 1,000 units a second, however warm the table.
| Peak write units per second, new table | What to do |
|---|---|
| Up to 4,000 | Nothing, if the writes spread across many partition keys. |
| Above 4,000, on a known date | Raise warm throughput ahead of time and wait for ACTIVE. |
| Above 4,000, without warning | Keep SDK retries on, and alarm on throttled requests, not on 5xx. |
Reads did not throttle before our fleet ran out
We seeded another fresh table with 1,000 items and read them with Eventually consistent read GetItem calls. Each ~1 KB read costs half a read unit.
At our highest rate, DynamoDB accepted every read we sent: 12,762 a second on average, with zero throttles. Five of our eight runners hit their own limit first.
That is about 6,400 read units a second, roughly half the documented 12,000 read units. We never found the read ceiling.
A laptop measures your route, and one Lambda measures itself
Our first load generator was a Node script on a laptop in Madrid. Each request to us-east-1 took 130 to 144 ms at the median.
The script paced 1,000 writes a second. Asked for 2,000, it delivered 107 a second with 1,500 requests in flight. DynamoDB throttled none of them, and 58 failed with other errors. The limit was our client and the route to Virginia.
Next we ran the same loop in one Lambda function in us-east-1, with 3,008 MB of memory. It paced 2,000 writes a second at a 10 ms median.
At 4,000 to 8,000 offered, it delivered 1,020 to 1,096 a second, held at its cap of 600 requests in flight. DynamoDB throttled none of those either.
We did not record CPU and we tried only one memory size. So we know only that one runner of this size cannot pace much past 2,000 writes a second.
The final instrument is eight such functions. Each paces one-eighth of the offered rate, and all start at a shared time 20 seconds ahead. The result is the sum of counted requests.
Each runner also reports when it hits its in-flight cap. In the write test, one runner did so in the 8,000 window. In the read test, five did at the highest rate.
In the sustained test, eight runners each ran four waves. A runner hit its cap in 25 of those 32 runs.
Three runs died before one worked
Our driver on the laptop first invoked each runner synchronously through the AWS CLI. That holds one HTTPS connection open, silent, for the whole run. Most likely our home router dropped connections that stayed idle that long.
Our notes record three runs broken this way. In one, a runner hung on a request that never resolved. The CLI timed out and retried the invoke on its own, and the re-run found its scheduled start long past.
The notes also record CLI read timeouts at 700 and 900 seconds, and they disagree on the rest. One says the functions had finished. Another says a runner hung until its function timeout and left no diagnostics.
The version that worked follows six rules:
- Invoke async and deliver results out of band. The invoke returns at once. Each runner writes its result to a small DynamoDB table, and the driver polls for eight rows.
- Turn retries off on every layer. The measurement client makes one attempt per request, because a retry would hide the throttles we count.
- Retry nothing upstream either. The CLI and the async invoke make one attempt. In almost all cases a duplicate fails loudly, because it finds its start time already past.
- Give every request a timeout. Ours is 8 seconds.
- Add a watchdog. One minute past its schedule, a runner returns partial counts and a note of where it was stuck.
- Cap the spend and clean up. Each runner aborts past a fixed number of operations, and a
finallyblock deletes everything the run created.
One table per run in one region leaves questions open
- Each number comes from one fresh table, measured once on August 27 or 28, 2026, in us-east-1, plus one September retest for the 10-second burst. None of them is a distribution.
- We sent single-item
PutItemand eventually consistentGetItemcalls of ~1 KB, with random keys and no indexes or streams.BatchWriteItemcan report throttled items asUnprocessedItemsinstead of an error. - Every write inserted a new key. We did not test updates to the same items.
- Retries were off. The AWS SDKs retry throttling errors by default, so an application sees slower writes before it sees errors.
- We did not find the read ceiling.
- All counts are client-side. The growth steps and the 10-second burst are shapes we observed, not causes we measured.
- Costs below are at list price.
What transfers if you measure a cloud service yourself
- Put the load generator in the same region as the target. Otherwise you measure the route.
- Shard the load across runners, record when each one saturates, and sum counted requests.
- Bucket finer than your window. Our 30-second averages hid a 10-second burst.
- Price the run first. At us-east-1 list price, the accepted requests cost about $0.49 for the ceiling run and $6.33 for the sustained run. Lambda, failed runs and probes are extra.
The on-demand throughput reference has every table from these runs, including the minute-by-minute growth data. DynamoDB throttling covers the other causes of that 400.
DynoTable can switch a table between on-demand and provisioned capacity. Warm throughput is set in the AWS console or CLI. Download DynoTable to work with your own tables.