If you Google or chat with any of the LLMs about “DynamoDB vs DocumentDB”, most answers are theoretical.
DynamoDB is supposed to be faster because it’s serverless. DocumentDB is supposed to be slower because it runs in a VPC, but no one is showing hard proof that this truly is the case. That’s why I built this experiment with 1000 requests with 200 concurrent users with two simple operations — writes and reads. And here are the results!
Full GitHub code is available here.
Experimental Setup
Both databases sit behind the same architecture: an API Gateway REST API invoking a Python 3.12 Lambda function inside a VPC, the Lambda connects to the database, executes the requested operation and returns the information back to our testing script.
For the DocumentDB stack, there are some additions — the DocumentDB instance needs to be inside a VPC, since it can only be set up in this way, and to use it, it requires credentials (username and password in this case). The database credentials are stored safely inside the Secrets Manager, which the Lambda will use to write and read from the DocumentDB instance.
DynamoDB is serverless by design — it scales automatically and you pay only for what you use. To make the comparison fair, I configured DocumentDB in Serverless mode as well (0.5–16 DCU, scaling on demand), so neither database has a fixed instance sitting idle between requests.
To make this experiment as reliable as possible, cold starts for Lambdas won’t be counted in the final result for both scenarios.
| DocumentDB | DynamoDB | |
|---|---|---|
| Instance / Mode | Serverless (0.5–16 DCU) | Serverless (pay-per-request) |
| Region | us-east-1 | us-east-1 |
| Lambda runtime | Python 3.12 | Python 3.12 |
| Lambda memory | 512 MB | 512 MB |
| Lambda timeout | 30 s | 30 s |
| Lambda location | Private subnet, VPC | Private subnet, VPC |
| Client library | pymongo | boto3 |
| Storage | Encrypted at rest | Managed by AWS |
| Test volume | 1,000 requests | 1,000 requests |
| Concurrency | 200 | 200 |
| Data | User records, ~400 bytes | Same schema, same payload |
Another few details to optimize the Lambda runtime and keep the experiment fair:
- MongoDB and DynamoDB clients are created outside the handler to allow connection reuse across invocations
- DynamoDB client is configured to have
ConsistentReadflag asFalse, to have quicker responses from the database
Here is the architecture diagram of this experiment, together with a short CDK snippet for creating a DocumentDB cluster and instance. The full code is available in the GitHub repository provided in the introduction.
# DocumentDB cluster
# Minimal 0.5 DCU configuration, maximum 16 DCU
self.docdb_cluster = docdb.CfnDBCluster(
self,
"DocDbCluster",
master_username=self.docdb_secret.secret_value_from_json(
"username"
).unsafe_unwrap(),
master_user_password=self.docdb_secret.secret_value_from_json(
"password"
).unsafe_unwrap(),
db_subnet_group_name=subnet_group.ref,
vpc_security_group_ids=[self.docdb_sg.security_group_id],
storage_encrypted=True,
engine_version="5.0.0",
deletion_protection=False,
serverless_v2_scaling_configuration=docdb.CfnDBCluster.ServerlessV2ScalingConfigurationProperty(
min_capacity=0.5,
max_capacity=16,
),
)
self.docdb_cluster.apply_removal_policy(RemovalPolicy.DESTROY)
# Serverless DocumentDB instance
docdb_instance = docdb.CfnDBInstance(
self,
"DocDbInstance",
db_cluster_identifier=self.docdb_cluster.ref,
db_instance_class="db.serverless",
auto_minor_version_upgrade=True,
)
docdb_instance.add_dependency(self.docdb_cluster)
The Results
POST — Writing Data
This is the operation that creates a user record in the database, with a payload very similar to this one:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "user-47291",
"timestamp": 1710249600,
"payload": "xK9mN2pQr5sTvW7yZbCdFgHjL4nPqRsTuVwXyZ0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz01"
}
And here are the results:
| Metric | DocumentDB | DynamoDB | Winner |
|---|---|---|---|
| P50 | 354.9 ms | 358.4 ms | DocumentDB (-1%) |
| P90 | 523.9 ms | 585.3 ms | DocumentDB (-10%) |
| P95 | 913.9 ms | 602.3 ms | DynamoDB (-34%) |
| P99 | 977.4 ms | 605.7 ms | DynamoDB (-38%) |
| Mean | 347.4 ms | 324.8 ms | DynamoDB (-7%) |
| Max | 3,033 ms | 905.4 ms | DynamoDB (-70%) |
| Throughput | 217 RPS | 508 RPS | DynamoDB (2.3x) |
| Failures | 0 / 1000 | 0 / 1000 | Tie |
Looking at P50, both databases have very identical performance — DocumentDB at 354ms, DynamoDB at 358ms. Up to P90, DocumentDB holds a slight edge, but nothing really dramatic.
Now we get to P95, where we start to see the difference — DocumentDB jumps to 913ms, while DynamoDB handles traffic 34% better by staying around the 600ms mark. In addition, P99 further confirms the wide gap between the write performances of these two NoSQL databases, DocumentDB almost hits a second, while DynamoDB stays in the 600ms range. The max duration of the request is the most dramatic part, DocumentDB takes 3 seconds, while DynamoDB stays under a second for the same operation.
Regarding the throughput, DynamoDB beats DocumentDB by 2.3x, which is a huge difference since the rest of the configuration is identical — similar VPC, Lambda and payloads. This is the VPC + connection model showing up under write pressure.
Both databases hit 100% success rate with no dropped requests.
GET — Reading Data
However, this is where things get interesting — the scenario is simple, Lambda gets invoked and fetches the user record by ID inside the database.
For DocumentDB, that's a find_one by indexed field. For DynamoDB, a get_item by partition key.
| Metric | DocumentDB | DynamoDB | Winner |
|---|---|---|---|
| P50 | 461.1 ms | 466.4 ms | DocumentDB (-1%) |
| P90 | 796.1 ms | 1,387.7 ms | DocumentDB (-43%) |
| P95 | 830.2 ms | 1,427.4 ms | DocumentDB (-42%) |
| P99 | 2,102.2 ms | 1,543.7 ms | DynamoDB (+36%) |
| Mean | 443.8 ms | 535.3 ms | DocumentDB (-17%) |
| Max | 2,394.7 ms | 1,786.3 ms | DynamoDB |
| Throughput | 380 RPS | 285 RPS | DocumentDB (1.3x) |
| Failures | 0 / 1000 | 0 / 1000 | Tie |
This result truly surprised me. At P50, DynamoDB has a small edge on reads, which was expected, however the difference at P90 and later changed the way I look at DocumentDB, as it provided better read performance than DynamoDB by around 40% — if you look at P90, DocumentDB just stays below the 800ms mark, while DynamoDB almost hits the 1.4 second mark, and the situation is very similar for P95.
A possible explanation of why DocumentDB beat DynamoDB for reads is the buffer cache. DocumentDB keeps frequently accessed documents in memory on the instance. Once the cache is warm — which is exactly the steady-state condition in production — repeated reads on the same dataset are served from RAM rather than storage.
At P99, DynamoDB regains its composure and beats DocumentDB by being faster 36%. A likely explanation is connection pool exhaustion, as the slowest 1% of requests are possibly queued and are waiting for a pymongo connection to free up.
Regarding the throughput, DocumentDB processes the read requests faster by 1.3x.
Again, as before, both databases maintained 100% success rate.
What the Numbers Actually Tell You
For writes under load: DynamoDB is the clear winner here. The tail latency difference is too significant and is going to impact user experience in every way. If you need a database for ingesting events, logging and creating records which have unstructured data, DynamoDB is the definite winner.
For reads under load: DocumentDB wins, and by a meaningful margin at the tail. The throughput gap is also notable, as DocumentDB serves 33% more read requests per second in this test.
An important statistic to mention — P50. Why? Because both databases have it within 10% of each other in both operations, so if you stopped here, you might have an argument that they have identical performances. However, the true difference live in P95 and P99 stats, where your slowest users are impacted the most.
Pricing: What Does This Actually Cost?
Performance is only a part of your architectural decision. Here's what running each database actually costs at the scale tested.
Lambda (same for both)
Lambda pricing is identical regardless of which database you use:
- Compute: $0.0000166667 per GB-second. At 512 MB and ~700ms average duration: roughly $0.006 per 1,000 requests
- Requests: $0.20 per million invocations
- At 1 million requests/month: ~$6 in compute + $0.20 in requests = ~$6.20/month
Lambda is not the cost driver here. The database is.
DynamoDB (On-Demand, us-east-1)
DynamoDB on-demand charges per request:
- Writes: $0.625 per million write request units (1 WRU = 1 KB write)
- Reads: $0.125 per million read request units (1 RRU = 4 KB strongly consistent read)
- Storage: $0.25 per GB-month
For our ~400 byte payload (rounds up to 1 KB write, well under 4 KB read):
| Volume | Writes | Reads | Storage (10GB) | Monthly Total |
|---|---|---|---|---|
| 100K req/day (~3M/month) | $1.88 | $0.38 | $2.50 | ~$4.76 |
| 1M req/day (~30M/month) | $18.75 | $3.75 | $2.50 | ~$25 |
| 10M req/day (~300M/month) | $187.50 | $37.50 | $2.50 | ~$227 |
No instance cost, as DynamoDB is a serverless NoSQL database.
DocumentDB (Serverless, us-east-1)
Pricing dimensions:
- Compute: ~$0.0822 per DCU-hour (Standard config, us-east-1)
- Storage: $0.10 per GB-month
- I/O: $0.20 per million requests
- Minimum capacity: 0.5 DCU (~1 GB RAM equivalent)
| Volume | Avg DCU | Compute | Storage (10GB) | I/O | Monthly Total |
|---|---|---|---|---|---|
| 100K req/day (~3M/month) | 0.5 | $29.59 | $1.00 | $0.60 | ~$31.19 |
| 1M req/day (~30M/month) | 1 | $59.18 | $1.00 | $6.00 | ~$66.18 |
| 10M req/day (~300M/month) | 2 | $118.37 | $1.00 | $60.00 | ~$179.37 |
This table is based on my CloudWatch statistics, where the database with 200 concurrent request reached 9.15 DCU, and during testing my random testing to see if the database works, the average DCU was 4.66 as seen in the image below.
The Cost Crossover
At low volumes, DynamoDB is dramatically cheaper, however, at very high traffic volumes, DocumentDB's per-I/O pricing ($0.20/million) becomes competitive with DynamoDB's per-request pricing ($0.625/million writes).
For this benchmark workload, the crossover point appeared around 5–10M requests/day.
When to Use Each
| Scenario | Recommendation |
|---|---|
| Low traffic, unpredictable spikes | DynamoDB — zero idle cost |
| High write concurrency | DynamoDB — better tail latency on writes |
| High read concurrency | DocumentDB — better tail latency and throughput on reads |
| Simple key-value access patterns | DynamoDB — purpose-built for this |
| Complex queries, aggregations | DocumentDB — native $match, $group, $sort pipeline |
| Existing MongoDB codebase | DocumentDB — minimal migration friction |
Lessons Learned
Measure before assuming. From my experience, it was always assumed that DynamoDB always provides best performance in the serverless infrastructure, but as we’ve proven with the read results at high concurrency, DocumentDB’s warm connection pool and buffer cache provide better tail latency than DynamoDB on-demand.
Tail latency is the real benchmark. Both databases are within 10% of each other at P50. The decision lives at P95 and P99. For write-heavy apps, that tail strongly favours DynamoDB. For read-heavy apps, it favours DocumentDB.
VPC overhead is real but manageable. DocumentDB lives in a VPC. The TCP/TLS handshake on cold starts adds latency. These are architectural decisions you have to live with if you go with DocumentDB, knowing that your reads are going to be better for your tail users.
Reproducing This Yourself
The full CDK stack (Python) and benchmark script are available on GitHub. Deploy it in your own account and run the same test — the numbers will vary by region, instance size, and concurrency, which is exactly the point. Your workload will tell you which database wins for your use case.
# Deploy from the CDK folder
cd cdk && cdk deploy
# Seed the databases with random data
python scripts/seed_dynamodb.py --table-name benchmark-users --count 1000
python scripts/seed_documentdb.py --stack-name DocDbApiStack --region us-east-1 --count 1000
# Run the benchmark for write operations
python api_load_test.py \
--endpoint https://<API_ENDPOINT_DYNAMODB_STACK>/prod/users \
--requests 1000 --concurrency 200 \
--method POST --output results_dynamodb_post.json
python api_load_test.py \
--endpoint https://<API_ENDPOINT_DOCUMENTDB_STACK>/prod/users \
--requests 1000 --concurrency 200 \
--method POST --output results_docdb_post.json
# Run the benchmark for read operations
python api_load_test.py \
--endpoint https://<API_ENDPOINT_DYNAMODB_STACK>/prod/users \
--requests 1000 --concurrency 200 \
--method GET --output results_dynamodb_post.json
python api_load_test.py \
--endpoint https://<API_ENDPOINT_DOCUMENTDB_STACK>/prod/users \
--requests 1000 --concurrency 200 \
--method GET --output results_docdb_get.json


Top comments (0)