research

We timed 65 DynamoDB restores. You can't predict the 66th.

Jeff Reynar
We timed 65 DynamoDB restores. You can't predict the 66th.

If you are writing a recovery plan that involves DynamoDB, you need to know how long a restore will take, but there isn’t good documentation about this. AWS says “the time it takes you to restore a table varies based on multiple factors” and that “point-in-time restore times are not always correlated directly to the size of the table”, then declines to estimate. The only figure they publish is from a support article last updated in May 2026: a point-in-time restore “takes at least 20 minutes, regardless of the table size.” Four paragraphs later the same article says “You can’t calculate the exact table restoration time in advance.”

We restored our production tables sixty-five times to find out. The 20-minute floor is wrong by a factor of five. AWS advises excluding secondary indexes to make a restore faster, and doing that made recovery six to nine times slower. The same table restored twice can differ by a factor of three.

How we measured

Every run issues a point-in-time restore with restore-table-to-point-in-time, to a fresh table, in the same region and account as the source. All ten tables are on-demand (PAY_PER_REQUEST) in us-east-1, in one account, restored September 10 to 12, 2026. A provisioned table may behave differently and we haven’t tested one.

The clock runs from the API call to the moment the table and every one of its secondary indexes reports ACTIVE. That matters, because it’s the slower of the two obvious metrics and it’s the one that corresponds to the application being fully operational again.

These times exclude validation. After each run we counted both tables with consistent scans and compared sampled items, which no one recovering from an outage does before declaring success. Every one of the sixty-five restores succeeded. The differences we did find between a copy and its source were the live table moving on, not data loss.

Polling adds up to one interval to a reading. The first tests polled describe-table every 30 seconds; every later run polled at one or five seconds. Thirty seconds is material on a four-minute restore and noise on a fifty-minute one, so the 30-second figures are starred in the table below, and every small table was re-run at one-second polling. Where the two intervals measured the same table they agreed: the 834 MB table gave 9.7, 8.2, 8.2, 8.2 at 30 seconds and 8.3, 8.3, 8.2, 8.5 at one second.

The measurements

Ten tables, sizes as reported by describe-table, times for a complete restore.

SizeItemsIndexesRestore
12.1 MB46933.6, 3.7, 4.6* min
18.5 MB12,80093.9, 5.1*, 6.6 min
76.1 MB204,48745.1, 5.1*, 5.1 min
100.9 MB41,62114.3, 5.0, 5.6* min
165.6 MB202,73744.9, 5.6*, 6.4 min
204.3 MB241,20415.1*, 5.3, 6.0 min
691.2 MB186,13827.8 to 12.1 min, eighteen runs
834.2 MB223,93806.7 to 20.9 min, twenty-five runs
3.09 GB10,567,744251.8*, 61.7* min
16.24 GB8,689,000692.9*, 91.4 min

* Polled at 30 seconds, so it can read up to 30 seconds long. On the starred long restores that is under 1% of the total. The 691 MB and 834 MB ranges include the 30-second batch too, one run and four; both rows’ endpoints come from finer polling.

The 20-minute floor does not exist

The fastest complete restore took 3 minutes 36 seconds, measured at one-second polling. Six of the ten tables finished in under seven minutes, indexes included. Whatever produced the 20-minute figure, it isn’t a floor you should plan around, and if your tables are small you are allowing four to five times the time you need.

The published guidance errs in both directions. Dynobase’s PITR guide, which ranks near the top of search results about this, says “Regardless of the database table size, it will only take a few seconds to restore.” Our fastest was 3 minutes 36 seconds and our slowest was 92.9 minutes, and table size is one of the two things that predicts it.

Restoring the data first and the indexes after is slower

AWS’s advice is that “restores can be faster and more cost-efficient if you exclude some or all indexes from being created on the restored table,” and their support article puts it as a sequence: exclude the indexes, add them once the restore completes.

We ran both conditions on one table, 12 MB with three global secondary indexes, both arms at 30-second polling, a bias both sides share and one that is small against the gap they show.

Table ACTIVEFully recovered
Complete restore4.6 min4.6 min, indexes included
GSIs excluded2.6 min28.5 min

The table came back 44% sooner and was fully operational about six times later, because DynamoDB creates the excluded indexes one at a time afterwards. If your application reads through a secondary index, a table without one isn’t a recovered table.

The obvious objection is that a bigger table would behave differently, because the data restore dominates and the indexes stop mattering. So we ran it again on a 3.09 GB table with 10.6 million items and two indexes.

Table ACTIVEFully recovered
Complete restore56.8 min56.8 min, indexes included
GSIs excluded46.4 min431.5 min, or 7.2 hours

It gets worse with size, not better. The penalty grew from 6.2x to 7.6x, and the head start shrank from 43% to 18%. The indexes built one after another both times: 209 and 188 minutes on the first run, 198 and 175 on the second.

Every figure in that table is the mean of two runs. The complete restores took 51.8 and 61.7 minutes, the excluded-index restores 443.3 and 419.7. Pick the fastest complete restore and the slowest excluded one, which is what a single pair of runs might have handed you, and the same experiment reads 8.6x.

What we still haven’t tested: excluding some indexes rather than all of them, and local or vector indexes.

One part doesn’t depend on our measurement at all. RestoreTableToPointInTime accepts a LocalSecondaryIndexOverride as well as a global one, so you can exclude local secondary indexes on restore the same way. UpdateTable has no matching parameter: it takes GlobalSecondaryIndexUpdates and VectorIndexUpdates, and nothing for LSIs, because a local secondary index can only exist from the moment a table is created. So “you can add indexes after the restoration completes” holds for global indexes and does not hold for local ones. Exclude an LSI on restore and the only way to get it back is to restore again.

Four at once slowed the worst by 12%, and there is a ceiling at 50

You’re lucky if your recovery only requires restoring one table. We ran four restores concurrently. Against the same tables running alone they finished 12%, 9%, 9% and 1% slower. The data path doesn’t serialize.

That is measured at four. It says nothing about forty, and there is a documented ceiling on how many you can run at all. From the API reference:

Any number of users can execute up to 50 concurrent restores (any type of restore) in a given account.

Two more limits are described on the same page. Up to 500 simultaneous table operations per account, counting restores among them, and up to 250 concurrent requests when creating tables with one or more secondary indexes, which every restore of an indexed table is. AWS adds that where table or index specifications are complex, “DynamoDB might temporarily reduce the number of concurrent operations.”

If you have more than 50 tables to bring back, they must be restored in waves. Your floor is not the slowest table. It is the slowest table in the first wave plus the slowest in the next, and so on. Start the largest tables first, so a long restore runs while short ones cycle through the remaining slots. Work out how many waves you need before you write a recovery time down, and don’t assume the 9% holds at fifty, because we haven’t measured that.

The same table restored twice can differ by 3x

We restored one 834 MB table twenty-five times. Twenty-three of those runs landed between 6.7 and 9.3 minutes. One landed at 9.7. One landed at 20.9.

A 691 MB table, eighteen runs, has the same problem on a smaller scale: fourteen runs inside 7.8 to 8.9 minutes, then 9.7, 9.8, 11.9 and 12.1.

It isn’t a big-table phenomenon. An 18 MB table restored in 3.9 minutes one evening and 6.6 minutes two days later, both at one-second polling, a 1.7x gap on a table that fits in memory.

The spread survives holding the protocol still. Twenty of those runs were taken as one controlled series: ten per table, alternating, every one restoring to a point exactly 24 hours old, back to back across three hours. Nineteen of the twenty fell between 8.0 and 9.3 minutes. One took 12.05.

It isn’t a slow patch you could wait out, either. That 12.05 started at 12:52. The other table restored at 12:44 in 8.28 minutes and again at 13:04 in 8.01.

That isn’t a bell curve with a tail. It’s a tight cluster plus an occasional run that lies outside it. Planning with an average and a margin assumes the first shape. The slow run isn’t the average plus 20%, it’s two and a half times the median, and it arrives without warning.

Index count explains none of it. The table with zero indexes produced the 3x outlier, and the six-index table was the steadiest thing we measured.

Nothing else we recorded explains it either: not size, not item count, not time of day, not how far back the restore point sat. The obvious candidate is load on the fleet underneath, which a customer account can’t see.

Reaching further back doesn’t slow it down

Point-in-time recovery covers 35 days, and the intuition is that reaching further back might take longer. We restored the same 834 MB table to three points in the window, then repeated the whole sequence an hour later.

Restore pointFirst passSecond pass
1 hour old8.68 min20.94 min
7 days old8.51 min7.83 min
34 days old7.11 min6.74 min

Both times the oldest restore point was the fastest. Reaching back a month doesn’t slow the restore, so you can plan recovery without caring where in the window the incident sits.

The reverse reading is tempting and we can’t support it. Both passes put the newest restore point first, so age and position in the batch are the same variable here, and we never swapped the order to separate them. The 20.94 is also the slowest of twenty-five runs on that table, which makes it the outlier from the previous section rather than a measurement of anything.

A third problem belongs to this table specifically. It’s a log table, so it grows, and an older restore point rebuilds a smaller copy of it. That would push in exactly the direction we observed. We can’t net it out either, because DescribeTable reports zero bytes and zero items on a freshly restored table, so the copy’s own size never gets recorded. Two passes agreeing on the ordering is suggestive, but suggestive isn’t evidence.

A model predicts the table. It can’t predict the restore.

Look at the two largest tables. One is 3.09 GB with 10.6 million items and restores in 51.8 and 61.7 minutes. The other is 16.24 GB with 8.7 million items and takes 92.9 and 91.4. More items, less time.

Throughput across the three largest ranges from 51 to 182 MB per minute, and from 27,000 to 204,000 items per minute. No single number you can read off describe-table gives you a usable rate.

Two of them together do, and better than we expected. Fitting minutes against table size and item count across the original ten tables:

minutes ≈ 4.6 + 0.0033 × MB + 4.0 × million items

A fit on ten points with three parameters is worth very little on its own, and what follows is not a validation of it. Both of these tables were in the fit, at two runs each. Restoring them 43 more times tests something narrower than generalization: whether two runs were enough to find the median those two points were standing in for.

Model saysMedian of the runsMiss
834 MB, 223,938 items, 25 runs8.25 min8.29 min2 seconds
691 MB, 186,138 items, 18 runs7.63 min8.73 min1.1 min

The model landed the median of twenty-five restores within two seconds, and it was 2.5x wrong about the restore we actually got. That same table produced a 20.94-minute run against a predicted 8.25.

This is the whole problem with planning a recovery off a formula. Predicting the average of a distribution is a different task from predicting the sample you’re about to draw, and a recovery draws exactly one.

AWS does explain the mechanism, and the explanation uses a number you cannot see. Their support article says that “if DynamoDB evenly distributes the data in your table, then the restoration time is proportional to the largest single partition by item count.” Note the conditional: the rule they give is stated for tables without skew, and skew is the case they tell you to worry about. There is no API that reports how many physical partitions a table has or which items live on which one. So the only published guidance is expressed in a quantity no customer can observe, which goes some way to explaining why nobody publishes measurements.

You can count your partition keys and see whether one holds far more items than the rest, which is worth knowing on its own, since that’s the language AWS’s own skew guidance is written in. It still doesn’t get you their number. Which keys share a physical partition, and how many partitions exist at all, is opaque, so counting keys tells you about hot keys rather than about partition load. The familiar 10 GB-per-partition rule of thumb has the same gap: it gives you a lower bound on how many partitions exist, never the item count of the biggest one.

One thing that looks alarming and is not

Every restored table came back holding fewer rows than the live one. On the 16 GB table the gap was 26,176 items.

That isn’t data loss. It’s the writes the live table took between the restore point and the moment we counted, and it’s the confirmation that the timestamp was honored. A restored copy that matched the live table would mean the restore point was ignored.

What to write in the runbook

Measure your own tables. Nothing here transfers directly, because the numbers depend on how your data sits across partitions, which you can’t inspect, and the rates vary by a factor of three across our own tables.

Time to all indexes ACTIVE, not to table ACTIVE. They happen together on a complete restore, so the number is the same, and it stays correct if that ever changes.

Don’t plan off a multiplier. Doubling the modelled time covers nine of our ten tables and misses the one that matters: the 20.9-minute run was 2.54x its modelled 8.25. Covering it takes 2.6x, and we can’t tell you 2.6x is enough, because we saw three outliers in the forty-three runs of those two tables and a longer series may well find a worse one. Track your own worst observed run instead of an average plus a margin.

Don’t exclude indexes to go faster. We never saw it reduce total restore time, on either table we tried it on, despite what AWS says.

Expect the restored table to be smaller than the source, and check that the difference matches your write volume since the restore point rather than treating it as corruption.

Drill it, and drill it again when the table changes

Every number above came from restoring real tables and timing them, which is the only method that produced anything trustworthy. It’s also available to you, takes one restore, and answers the question for your data instead of ours.

AWS already tells you to do this, in the same article the 20-minute number comes from: “Document your average restore completion time regularly”, then work out what those times do to your RTO. Do it, and keep a different statistic. The average is the one number an outlier barely moves.

Drill your large tables on a schedule. The small ones finish in minutes and the error bars don’t matter. The large ones are where a recovery estimate is load-bearing, and where being 2.5x wrong changes what you tell people during an outage. Run each of them more than once, because a single drill gives you one sample from a distribution whose interesting feature is the tail.

Re-drill when a table grows. Both terms that fit our data scale with the table, so a table that has doubled since its last drill has a different answer, and the drill is the only place that answer exists.

Re-drill when someone adds an index. Index count didn’t move our restore times, but that isn’t a reason to skip this. Adding a global secondary index changes what “recovered” means, because the application isn’t fully operational until every index reports ACTIVE, and it changes how badly the exclusion strategy would hurt you if you were ever planning to use it. We tested up to nine indexes on ten tables in one account, which isn’t enough to promise you anything about yours.

Drill the recovery, not the table. Time the whole sequence you’d actually run, including the tables that wait behind the 50-restore ceiling, and write down the slowest run you’ve seen rather than the one you’d like to quote.