DynamoDB Export to S3 and Import from S3: Full and Incremental Exports, CSV and Parquet with Athena, Existing Tables, Pricing and Errors
To export a DynamoDB table to S3, turn on point-in-time recovery (PITR) and run aws dynamodb export-table-to-point-in-time --table-arn ... --s3-bucket ... (or choose Exports to S3 in the console). The export is managed and asynchronous, uses no read capacity, and writes compressed DynamoDB JSON or Amazon Ion files plus manifests; you can export the full table at any point in your PITR window, only the changes between two times (incremental export), or a filtered subset. It costs about $0.10 per GB of table size in US East. To get CSV or Parquet, query the export with Athena and write the format you need. Going the other way, import from S3 loads CSV, DynamoDB JSON or Ion files (optionally GZIP or ZSTD) into a new table without using write capacity, at about $0.15 per GB; importing into an existing table needs a script or AWS Glue instead. Below: step-by-step commands, the output layout, incremental and filtered exports, CSV and Parquet, cross-account exports, imports with Terraform, costs, and every error.
The shop's finance team wanted every order since launch in a spreadsheet, and the analytics team wanted the same data in Athena. Jake's first idea was a Scan in a Python loop, which would have read the whole 40 GB table through provisioned capacity and slowed the live site. An export to S3 did it without touching the table's capacity at all, and Athena turned the result into both a CSV and Parquet in one afternoon.
New to DynamoDB or Athena? The cards below cover DynamoDB itself, Athena on S3, batch writes and DynamoDB costs.
Every way to get data out of (and into) DynamoDB
| Method | Uses table capacity? | Best for |
|---|---|---|
| Export to S3 (full) | No | Whole-table snapshots for analytics, archives, migrations |
| Export to S3 (incremental) | No | Only what changed between two times, to keep a copy in sync |
| Console "download results as CSV" | Yes (it is a scan or query) | A few hundred items, quickly |
| Scan in a script | Yes, heavily | Small tables; custom transforms |
| AWS Glue job | Can read from an export (no) or the table (yes) | Regular ETL into Parquet or a data lake |
| Import from S3 | No (new table only) | Loading a large data set into a fresh table |
| BatchWriteItem script | Yes | Loading into an existing table |
The managed export and import exist precisely so that bulk data movement does not compete with your application for capacity. Our DynamoDB explainer covers capacity and keys if they are new to you.
Export a DynamoDB table to S3, step by step
- Turn on point-in-time recovery (required for exports):
aws dynamodb update-continuous-backups --table-name Orders --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true. PITR has its own storage charge, and it also protects you against accidental deletes. - Pick a bucket (same or another Region or account) and a prefix, such as
s3://shop-exports/orders/. - Start the export:
aws dynamodb export-table-to-point-in-time \ --table-arn arn:aws:dynamodb:eu-west-2:111122223333:table/Orders \ --s3-bucket shop-exports --s3-prefix orders/2026-10-20 \ --export-format DYNAMODB_JSON
- Watch it:
aws dynamodb describe-export --export-arn ARN --query "ExportDescription.[ExportStatus,ItemCount,BilledSizeBytes]", or the Exports to S3 page in the console. Status goes fromIN_PROGRESStoCOMPLETED(orFAILEDwith a reason). - Use the files: query with Athena, load with Glue, or keep them as an archive.
Add --export-time 2026-10-19T23:59:00Z to export the table as it was at that moment (anywhere in the PITR window, up to 35 days back), which is how you get a consistent end-of-day snapshot without stopping writes. Exports have no completion-time guarantee: a 40 GB table usually takes tens of minutes, but plan workflows around the status, not a fixed timer.
Jake: "And the live site didn't notice?"
Ethan: "Exports read from the backups, not the table. Zero read capacity used."
The same export from Python. When an export is one step in a pipeline, start it and wait for it in code:
import time
import boto3
ddb = boto3.client("dynamodb", region_name="eu-west-2")
resp = ddb.export_table_to_point_in_time(
TableArn="arn:aws:dynamodb:eu-west-2:111122223333:table/Orders",
S3Bucket="shop-exports", S3Prefix="orders/2026-10-20",
ExportFormat="DYNAMODB_JSON",
)
arn = resp["ExportDescription"]["ExportArn"]
while True:
d = ddb.describe_export(ExportArn=arn)["ExportDescription"]
if d["ExportStatus"] != "IN_PROGRESS":
break
time.sleep(60)
print(d["ExportStatus"], d.get("ItemCount"), d.get("ExportManifest"), d.get("FailureMessage"))
The ExportManifest value is the S3 key of the summary manifest, which is the reliable way to find the export's folder, since the folder name includes an ID you cannot predict. Polling once a minute is plenty; exports take minutes to hours. For a scheduled pipeline, put the same two calls in a Step Functions state machine (it can call the DynamoDB export API directly and wait in a loop without paying for an idle Lambda function), and start it from an EventBridge Scheduler schedule each night.
Why exports need PITR, and what PITR costs
Exports read from DynamoDB's continuous backups rather than from the table, which is why they use no capacity, and why point-in-time recovery must be on. PITR keeps a rolling record of every change for up to 35 days (you can choose a shorter recovery period), so an export can pick any second in that window.
PITR is billed on the table's size, roughly $0.20 per GB-month in US East, for as long as it is on. For the shop's 40 GB table that is about $8 a month, which buys two things at once: exports whenever they want them, and the ability to restore the table to any second in the window after a bad deploy or an accidental delete. Most teams that turn it on for exports keep it on for the protection alone. If cost matters for a very large, rarely exported table, you can turn PITR on before an export and off afterwards, but the recovery window starts from the moment it is enabled, and you lose the safety net in between, which is rarely a good trade.
What the export looks like in S3
s3://shop-exports/orders/2026-10-20/AWSDynamoDB/01760918400000-a1b2c3d4/
manifest-summary.json # export details: item count, billed size, format
manifest-files.json # one line per data file
data/
3f7k...q9.json.gz # compressed JSON lines, one item per line
...
_started # empty marker file; safe to delete
Each line of a data file is one item in DynamoDB's typed JSON, which keeps every attribute's type:
{"Item":{"orderId":{"S":"A-1042"},"total":{"N":"59.90"},"status":{"S":"shipped"},"items":{"L":[{"S":"jacket-blue"}]}}}
The summary manifest is a small JSON document worth reading first: it records the export's ARN, the start and end times, the output format, the item count and the billed size, and the S3 key of the file list. Automations should read it rather than guessing file names, because the folder includes an export ID, and the data file names are random.
There is at least one file per table partition, so even a small table produces several files. Athena, Glue and Spark read this layout directly; the manifest lists exactly which data files belong to this export, and manifest-summary.json holds the item count and billed size, which is a quick sanity check against the table's own item count.
Jake: "Why is the total field a string, "59.90", in quotes?"
Ethan: "DynamoDB JSON stores numbers as strings with an N type so nothing loses precision. You cast it when you query."
What an export contains: TTL, deletes and timing
An export is the table exactly as it was at the export time, which has a few consequences people notice in the numbers:
- Expired items may still be there. Time to live deletes expired items in the background, typically within a few days, not the moment they expire. Items past their expiry time but not yet removed appear in the export. Filter on your TTL attribute in Athena (
WHERE expires_at > to_unixtime(now())) if you want only live items. - Counts differ from the console. The item count in the table's overview is updated only every few hours, so it rarely matches the export's exact count; the export's manifest is the precise figure for that moment.
- Writes during the export are not in it. The export reflects the export time, even though it takes minutes or hours to finish; anything written later belongs to the next export or an incremental one.
- Global secondary indexes are not exported separately. The items are all there, and you can rebuild any index view in Athena; only local secondary index data counts towards the billed size.
Opening the result in Excel on Windows
The person who asked for the export usually wants it in Excel, and two habits make that painless. Open the CSV through Data, From Text/CSV rather than a double-click, and set ID columns to Text in the import preview, so long order numbers are not rounded into scientific notation and leading zeros survive. And mind Excel's limit of 1,048,576 rows per sheet: for bigger exports, split the CSV by month in the Athena query (one UNLOAD per month, or a partitioned_by clause), or load the file through Power Query into the data model, which handles far more rows than a sheet. If the spreadsheet is a recurring request, an Athena query the finance team can run themselves, or a QuickSight dashboard on the Parquet table, saves everyone the monthly file.
DynamoDB JSON or Amazon Ion?
Exports come in two formats, and the choice mostly depends on who reads the files next:
- DynamoDB JSON is plain JSON lines with type markers (
{"S": "..."},{"N": "..."}). Every tool reads JSON, and Athena's JSON SerDe handles it as shown below. It is the safe default. - Amazon Ion is a typed superset of JSON. It keeps DynamoDB's types (exact decimals, binary values, sets) without the wrapper objects, so the files are more compact and closer to the data. Athena can read Ion with its Ion SerDe, and Ion libraries exist for the main languages, but fewer everyday tools understand it.
For CSV, Parquet or a spreadsheet, either works, since you convert anyway; pick DynamoDB JSON unless your downstream team already works with Ion. For restoring into a new table with import, both formats are accepted.
Export DynamoDB to CSV or Parquet (with Athena)
The native export writes JSON or Ion, not CSV. Athena converts it in two statements. First, a table over the export's data folder:
CREATE EXTERNAL TABLE orders_export ( Item struct<orderId:struct<S:string>, total:struct<N:string>, status:struct<S:string>, createdAt:struct<S:string>> ) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' LOCATION 's3://shop-exports/orders/2026-10-20/AWSDynamoDB/01760918400000-a1b2c3d4/data/';
Then write it out in the format you want, with friendly column names:
-- Parquet for analytics
CREATE TABLE orders_parquet
WITH (format = 'PARQUET', external_location = 's3://shop-analytics/orders_parquet/') AS
SELECT Item.orderId.S AS order_id, CAST(Item.total.N AS decimal(10,2)) AS total,
Item.status.S AS status, Item.createdAt.S AS created_at
FROM orders_export;
-- CSV for the spreadsheet
UNLOAD (SELECT Item.orderId.S AS order_id, Item.total.N AS total, Item.status.S AS status FROM orders_export)
TO 's3://shop-reports/orders_csv/' WITH (format = 'TEXTFILE', field_delimiter = ',', compression = 'NONE');
Our Athena on S3 guide explains UNLOAD and CTAS in more depth, and our Athena costs guide shows why Parquet makes later queries cheaper. For a regular pipeline rather than a one-off, an AWS Glue job can read the export and write Parquet on a schedule; our Glue memory guide helps when big exports strain a job.
For a very small table, the console's item explorer can download the current scan or query results as CSV, which is quicker than any of this, but it uses read capacity and is limited to what the page shows.
Turn an export into CSV with Python (no Athena)
If you would rather not set up Athena for a one-off, a short script reads the export's files straight from S3 and writes CSV. It uses boto3's own type deserializer, so numbers, lists and maps come out as normal Python values:
import csv, gzip, io, json
import boto3
from boto3.dynamodb.types import TypeDeserializer
s3 = boto3.client("s3")
BUCKET = "shop-exports"
EXPORT = "orders/2026-10-20/AWSDynamoDB/01760918400000-a1b2c3d4/"
COLUMNS = ["orderId", "total", "status", "createdAt"]
td = TypeDeserializer()
manifest = s3.get_object(Bucket=BUCKET, Key=EXPORT + "manifest-files.json")["Body"].read().decode()
with open("orders.csv", "w", newline="", encoding="utf-8") as out:
w = csv.DictWriter(out, fieldnames=COLUMNS, extrasaction="ignore")
w.writeheader()
for line in manifest.splitlines():
key = json.loads(line)["dataFileS3Key"]
body = s3.get_object(Bucket=BUCKET, Key=key)["Body"].read()
for record in gzip.GzipFile(fileobj=io.BytesIO(body)):
item = {k: td.deserialize(v) for k, v in json.loads(record)["Item"].items()}
w.writerow(item)
This is fine for exports up to a few gigabytes on a laptop or a small EC2 instance. Beyond that, Athena or Glue is faster and does not need the whole file in memory. Two details to watch: numbers come back as Python Decimal, which the CSV writer prints correctly, and attributes that are lists or maps are written as their Python form, so flatten them first if the spreadsheet needs separate columns. Open the CSV in Excel with the "From Text/CSV" import rather than a double-click, so long order IDs are not turned into scientific notation.
Incremental and filtered exports
An incremental export writes only the items that changed between two times, so a downstream copy can stay current without exporting the whole table every night:
aws dynamodb export-table-to-point-in-time \ --table-arn arn:aws:dynamodb:eu-west-2:111122223333:table/Orders \ --s3-bucket shop-exports --s3-prefix orders-changes \ --export-type INCREMENTAL_EXPORT \ --incremental-export-specification ExportFromTime=2026-10-19T00:00:00Z,ExportToTime=2026-10-20T00:00:00Z,ExportViewType=NEW_AND_OLD_IMAGES
- The window must be between 15 minutes and 24 hours, inside the PITR period; chain exports for longer spans.
- Cost is based on the data processed from the continuous backups for that window, with a 10 MB minimum per export.
Each record has the item's keys and a write timestamp. What else it carries tells you what happened to the item:
| What happened in the window | View: new images only | View: new and old images |
|---|---|---|
| Item inserted | Keys + new image | Keys + new image |
| Item updated | Keys + new image | Keys + new image + old image |
| Item deleted | Keys only | Keys + old image |
| Inserted and deleted inside the window | Nothing | Nothing |
The "old image" is the item as it was at the start of the window, which lets a downstream system undo or audit a change. If you only need to upsert the latest state into a copy, new images only is smaller and simpler.
A filtered export (full or incremental) exports only part of the table. You can give a key condition (one partition key value, plus an optional sort key condition, which also cuts the data read and the bill), a filter expression on any attributes (applied after reading, so it does not reduce cost), and a projection to keep only some attributes. Handy for "all orders for one customer" or "only the columns finance needs".
Import from S3 into a new DynamoDB table
Import is the bulk loader. It always creates the target table as part of the request:
aws dynamodb import-table \
--s3-bucket-source S3Bucket=shop-imports,S3KeyPrefix=products/ \
--input-format CSV --input-compression-type NONE \
--table-creation-parameters '{"TableName":"Products","KeySchema":[{"AttributeName":"sku","KeyType":"HASH"}],"AttributeDefinitions":[{"AttributeName":"sku","AttributeType":"S"}],"BillingMode":"PAY_PER_REQUEST"}'
- Formats: CSV (header row with attribute names; values import as strings except the keys you define), DynamoDB JSON (including the data files of an export, which is how you restore an export into a new table), and Amazon Ion; uncompressed, GZIP or ZSTD.
- No write capacity used, and you can add global secondary indexes in the creation parameters (local secondary indexes are not supported).
- Duplicate keys overwrite each other: the last one wins, so the item count can be lower than the line count.
- Bad rows are skipped and logged to CloudWatch Logs; check the import's error count before trusting the table.
- Cross-account and cross-Region sources work if the importing identity can read the bucket.
In Terraform, the aws_dynamodb_table resource has an import_table block that runs this when the table is first created:
resource "aws_dynamodb_table" "products" {
name = "Products"
billing_mode = "PAY_PER_REQUEST"
hash_key = "sku"
attribute {
name = "sku"
type = "S"
}
import_table {
input_format = "CSV"
s3_bucket_source {
bucket = "shop-imports"
key_prefix = "products/"
}
}
}
Jake: "Can I import the CSV into the Products table we already have?"
Ethan: "Not with import. It only creates tables. For the existing one, it's a batch-write script."
Preparing a CSV for import
Most failed CSV imports fail on the file, not on DynamoDB. Check these before you start:
- A header row with attribute names as the first line, matching the key names in your table definition exactly (case matters). If the file has no header, pass the names with the input format options.
- The right delimiter: comma by default; files saved with semicolons (common from European Excel settings) need the delimiter set in the input format options, for example
--input-format-options '{"Csv":{"Delimiter":";"}}'. - UTF-8 encoding: save from Excel as "CSV UTF-8", and check that the first header name does not pick up an invisible byte-order mark; if the partition key "does not exist" in every row, that mark is the usual cause.
- A value for every key: rows with an empty partition key (or sort key) cannot be written and are logged as errors.
- Types in mind: everything except the key attributes arrives as a string. If you need numbers, booleans or lists, import DynamoDB JSON instead, or convert the attributes after the import.
Test with a small file first, into a throwaway table, and read the import's error count and log group. Ten minutes on a 100-row sample saves re-running a million-row import because of a semicolon.
Watching imports and exports. aws dynamodb list-exports and list-imports show recent jobs, and describe-import --import-arn ARN reports the status, processed item count, error count and the CloudWatch log group with the error details. An import that finishes with a non-zero error count is "completed", not "failed", so always read the counts rather than the status alone.
Load into an existing table (CSV or JSON)
Because import-from-S3 only creates new tables, loading into a live table uses ordinary writes, which consume write capacity:
import csv
import boto3
table = boto3.resource("dynamodb").Table("Products")
with open("products.csv", newline="", encoding="utf-8") as f, table.batch_writer(overwrite_by_pkeys=["sku"]) as batch:
for row in csv.DictReader(f):
batch.put_item(Item={k: v for k, v in row.items() if v != ""})
batch_writer groups writes into batches of 25 and retries unprocessed items for you; our batch write guide covers throttling and retries. For millions of rows, run it from AWS (Glue, Step Functions with a distributed map, or an EC2 instance near the table), and on provisioned tables raise write capacity during the load. Note that empty CSV cells are skipped above, since DynamoDB does not accept empty strings for key attributes, and numbers arrive as strings unless you convert them. Updating some attributes of existing items instead of replacing them is covered in our PutItem vs UpdateItem guide.
Glue, zero-ETL and other managed routes
Exports are one-off snapshots. For a pipeline that keeps analytics data fresh, there are more managed options:
- AWS Glue's DynamoDB export connector: set
dynamodb.exporttoddband the Glue job starts an export and reads it, instead of scanning the table, so it uses no read capacity. Setting it tos3reads an earlier export you name, skipping the new export entirely. Point-in-time recovery must be on. For small tables the scan-based connector can be faster; for large ones, the export route is the gentler choice. - Zero-ETL integrations: DynamoDB can feed Amazon Redshift and Amazon OpenSearch Service continuously through managed integrations, so analytics and search stay current without you running exports at all. They suit dashboards and search features that need data within minutes, rather than a nightly file.
- DynamoDB Streams with Lambda or Kinesis: for event-by-event processing (send a confirmation email, update a cache), streams react to each change as it happens; exports are for bulk copies.
A simple way to choose: a file for someone (finance, an auditor, a one-off migration), use an export; a table in a warehouse that must stay current, use zero-ETL or a Glue job on incremental exports; a reaction to each change, use streams.
Export, backup or point-in-time restore?
Three DynamoDB features sound alike and do different jobs:
- Point-in-time restore creates a new DynamoDB table from any second in the last 35 days. It is the answer to "someone deleted half the orders an hour ago".
- On-demand backups (or AWS Backup) keep full copies for as long as you choose, restorable to new tables, including in other Regions or accounts with AWS Backup.
- Export to S3 produces files for other tools to read. You can import them into a new table, but that is a data load, not a restore: it does not bring back indexes you did not define in the import, the table's settings, or its streams.
So keep PITR on for recovery (exports need it anyway), use backups for long-term retention, and use exports for analytics and data sharing. Treating exports as your only backup works until the day you need the table back quickly with all its settings.
Export to another AWS account or Region
Exports can go straight to a bucket in another account or Region. Pass the bucket owner's account ID, and give your exporting role write access through the destination bucket's policy:
aws dynamodb export-table-to-point-in-time \ --table-arn arn:aws:dynamodb:eu-west-2:111122223333:table/Orders \ --s3-bucket analytics-landing --s3-bucket-owner 444455556666 \ --s3-sse-algorithm KMS --s3-sse-kms-key-id arn:aws:kms:eu-west-2:444455556666:key/abcd-1234
The destination policy needs to allow the exporting role to put objects (and complete multipart uploads) under the prefix; if you encrypt with a KMS key in the other account, that key's policy must allow the role to generate data keys. Most failed cross-account exports are one of those two permissions; our S3 403 guide helps read the errors.
The same works across Regions: the bucket can live in a different Region from the table, which suits a central analytics account in one Region collecting exports from tables around the world. Data moving between Regions is charged as normal inter-Region transfer on top of the export fee, so for very large or frequent exports, a bucket in the table's own Region with replication to the central Region can be easier to manage. Also set the bucket's Object Ownership to "bucket owner enforced", so the analytics account owns every exported file without any extra ACL settings on the export.
Encrypting and protecting exports
An export is a full copy of the table's data sitting in S3, often including names, addresses and order histories, so treat the bucket as carefully as the table:
- Encrypt the files: choose
--s3-sse-algorithm AES256(S3-managed keys) orKMSwith a key you control. The table's own encryption does not carry over to S3; the export files are encrypted by the bucket settings and the options you pass. - Export only what is needed: a filtered export with a projection can leave out personal fields, so the analytics team gets order totals and dates without customer emails.
- Expire old exports: a lifecycle rule on the export prefix (for example 30 days) stops copies of personal data piling up for years, and stops the storage bill growing with them.
- Restrict and log access: a bucket policy that allows only the analytics roles, Block Public Access on, and CloudTrail data events on the export bucket if you must show who read the files.
Jake: "Finance only needs totals and dates. The export has customer emails in it."
Ethan: "Then use a projection and leave them out. What isn't exported can't leak from the bucket."
DynamoDB export and import pricing
- Full export: about $0.10 per GB of table size (items plus local secondary indexes) at the export time, in US East. The shop's 40 GB table: about $4 per export.
- Incremental export: per GB of change data processed for the window, 10 MB minimum.
- Filtered export: same rates; a key condition on one partition reduces the data billed.
- Import: about $0.15 per GB of uncompressed source data, including rows that fail to load.
- Around it: PITR storage for the table, S3 storage and requests for the files, and Athena or Glue for converting them.
The S3 side is usually small because export files are compressed: a 40 GB table often produces well under 40 GB of files, and at Standard storage rates a month of one full export costs well under a dollar. It grows quietly when every nightly full export is kept forever, which is why the lifecycle rule matters more than the export fee. The Parquet copy made by Athena is smaller again, and it is the copy worth keeping long term, since every later query on it scans a fraction of the JSON's bytes.
Export or Scan: the honest cost comparison. In raw dollars, a one-off Scan is often cheaper than an export. On an on-demand table, reading 40 GB with eventually consistent reads is about 5 million read request units, which at roughly $0.125 per million is well under a dollar (about double for strongly consistent reads), against about $4 for the export. What the export buys is everything around the price: it uses none of the table's capacity, so the live application never slows down or gets throttled; it gives one consistent point-in-time copy instead of a scan that runs while items change underneath it; it needs no code, retries or paging; and it scales to tables where a scan would take hours. On a provisioned table, a scan uses capacity you already pay for, but it competes with your application for it, which is exactly the problem the shop wanted to avoid. Small table, quiet hours, quick answer: a scan is fine. Large or busy table, or anything you will repeat: export. Our DynamoDB cost guide covers the read-request side.
Copy a whole table to another account or Region
Export plus import is also a clean way to copy a table between accounts or Regions, for a new environment or a migration:
- Export the table (full, at a chosen time) to a bucket the destination account can read, or straight into a bucket in the destination account with
--s3-bucket-owner. - Import the export's data files in the destination account with
import-table, format DYNAMODB_JSON, defining the same keys and any global secondary indexes. - Catch up on writes made after the export time: run incremental exports for the gap and apply them, or replay changes from DynamoDB Streams, then switch applications over.
- Recreate the settings the import does not copy: time to live, streams, auto scaling or on-demand mode, tags, alarms and PITR on the new table.
Two alternatives are worth weighing. AWS Backup can copy a DynamoDB backup to another account or Region and restore it there, which keeps more of the table's configuration in one step. And if the goal is the same table live in two Regions, global tables replicate continuously, with no export or import at all.
Querying the live table from Athena, the other route people find, uses the Athena DynamoDB connector, a Lambda-based federated query. It reads the table directly and so consumes read capacity on every query, which is fine for an occasional lookup and wrong for heavy analytics; for repeated reporting, query an export or a zero-ETL copy instead.
A worked nightly pipeline
Here is the shape the shop settled on, which fits most small and mid-sized teams:
- Once: a full export of the Orders table, converted to a Parquet table with Athena, as the starting point.
- Every night: an EventBridge Scheduler schedule starts a Step Functions state machine, which runs a 24-hour incremental export into a dated prefix and waits for it to complete.
- Then: an Athena query (or a small Glue job) merges the night's changes into the analytics table: new and updated orders replace their old rows, deleted ones are removed.
- Finally: a lifecycle rule expires the raw export files after 30 days, since the merged table holds the history that matters.
The costs stay small: the nightly incremental export bills only the changed data (with the 10 MB minimum), Athena scans only one night's files plus the target table's affected partitions, and Step Functions and Scheduler cost cents a month. Compared with a nightly full export of a 40 GB table (about $4 a night, $120 a month), incremental exports of a few hundred megabytes of daily change cost a tiny fraction. If the analytics side later needs data within minutes rather than by morning, that is the point to look at a zero-ETL integration instead.
Before your first export: a checklist
- Turn on PITR for the table, and note that it adds a storage charge based on table size.
- Create or choose the bucket, with encryption, Block Public Access, bucket owner enforced and a lifecycle rule on the export prefix.
- Grant permissions:
dynamodb:ExportTableToPointInTimeon the table, and S3 write (plus KMS if used) on the bucket for the role that runs it. - Decide full, incremental or filtered, and which attributes really need to leave the table.
- Estimate the cost from the table size on the console's overview: about $0.10 per GB for a full export.
- Plan the conversion: Athena for CSV and Parquet, the Python script for a quick one-off, Glue for a regular pipeline.
Everything on this list is a one-time setup; after it, each export is a single command. Keep the command, or the Python version, in your team's runbook next to the table's name, so the next request for "all orders as a spreadsheet" takes minutes rather than a morning.
Errors and fixes
- "Point in time recovery is not enabled for table": turn on PITR, then start the export.
- Export
FAILEDwith an S3 access error: the role cannot write to the bucket or use its KMS key; check the bucket policy and key policy, especially across accounts. InvalidExportTimeException: the export time is outside the PITR window (older than 35 days, or in the future).- Incremental export rejected: the window is shorter than 15 minutes or longer than 24 hours, or starts before PITR was turned on.
- Import completed with errors: some rows failed parsing; read the CloudWatch log group named in the import description, fix the source, and import again into a new table.
- Fewer items than rows after import: duplicate keys overwrote each other.
- Athena returns empty columns: the struct names in your table definition do not match the attribute names or types exactly (they are case-sensitive in the JSON).
ResourceInUseExceptionon import: a table with that name already exists; import needs a new name.
Frequently asked questions
How do I export a DynamoDB table to S3?
Enable point-in-time recovery, then run aws dynamodb export-table-to-point-in-time with the table ARN and bucket, or use Exports to S3 in the console.
Does DynamoDB export to S3 consume read capacity?
No. It reads from continuous backups, so it does not affect table performance.
How much does DynamoDB export to S3 cost?
About $0.10 per GB of table size for a full export in US East, plus S3 storage and PITR.
How do I export DynamoDB to CSV?
Export to S3, create an Athena table over the export, and UNLOAD the columns you want as CSV; for tiny tables, download scan results from the console.
Can DynamoDB export to Parquet?
Not directly; convert the JSON export to Parquet with an Athena CTAS query or an AWS Glue job.
What formats does DynamoDB export use?
DynamoDB JSON or Amazon Ion, as compressed JSON-lines files plus manifest files.
What is a DynamoDB incremental export?
An export of only the items changed between two times (15 minutes to 24 hours apart), with new and optionally old images.
How do I import a CSV into DynamoDB?
Use import-table with --input-format CSV to create a new table, or a batch_writer script for an existing table.
Can I import from S3 into an existing DynamoDB table?
No. Import from S3 always creates a new table; use BatchWriteItem or Glue for existing tables.
How much does DynamoDB import from S3 cost?
About $0.15 per GB of uncompressed source data, with no write capacity used.
How do I restore a DynamoDB export into a table?
Run import-table with DYNAMODB_JSON format pointing at the export's data files; it creates a new table.
Can I export DynamoDB to another AWS account?
Yes. Pass --s3-bucket-owner with the bucket's account ID and allow your role in the bucket and KMS key policies.
How long does a DynamoDB export take?
There is no guaranteed time; it depends on table size and distribution. Track the export status instead of a timer.
Do I need point-in-time recovery to export?
Yes, PITR must be enabled on the table for both full and incremental exports.
Can I export only part of a DynamoDB table?
Yes, with a filtered export using a key condition, filter expression and projection.
How do I query a DynamoDB export with Athena?
Create an external table over the export's data folder with the JSON SerDe and Item struct columns, then query it.
What is AWS Glue's role in DynamoDB to S3?
Glue jobs can read exports (or the table) and write transformed Parquet or CSV on a schedule.
Does import support global secondary indexes?
Yes, you can define GSIs when the import creates the table; local secondary indexes are not supported.
How big are DynamoDB export files?
They are compressed, so usually smaller than the table size; the billed size is the table size, not the file size.
How much does DynamoDB point-in-time recovery cost?
Roughly $0.20 per GB-month of table size in US East while it is enabled, and it is required for exports.
Can I turn PITR off after exporting?
Yes, but you lose point-in-time restore, and the next export needs it on again, with a recovery window that starts from re-enabling.
Does a DynamoDB export include items expired by TTL?
It can: expired items not yet deleted by TTL are still in the table at export time, so filter on the TTL attribute in Athena.
Why does the export item count differ from the console?
The console's item count updates only every few hours; the export's manifest has the exact count for the export time.
How do I open a large DynamoDB CSV in Excel?
Use Data, From Text/CSV with ID columns as Text, and split files over 1,048,576 rows or load them through Power Query.
What permissions does a DynamoDB export need?
dynamodb:ExportTableToPointInTime on the table, plus S3 write permission (and KMS key use, if encrypting) on the destination bucket.
How do I copy a DynamoDB table to another account?
Export it to a bucket the other account can read, import the data files there with import-table, catch up recent writes, then recreate settings like TTL and streams.
Does import-table copy TTL, streams and auto scaling?
No. It creates the table with the keys and indexes you define; set time to live, streams, capacity settings and PITR afterwards.
Can Athena query a DynamoDB table directly?
Yes, with the Athena DynamoDB connector, but it reads the live table and consumes read capacity; query an export for heavy analytics.
Should I export DynamoDB as JSON or Ion?
DynamoDB JSON is the safe default that every tool reads; Ion is more compact and keeps types exactly, if your downstream tools support it.
Why does my CSV import say the partition key is missing?
Usually the header name differs from the key (case matters), the delimiter is wrong, or a byte-order mark sits before the first header name.
How do I import a semicolon-separated CSV into DynamoDB?
Pass --input-format-options with a Csv Delimiter of ";" to import-table.
Are DynamoDB exports encrypted?
They are encrypted by the S3 options you choose, SSE-S3 or SSE-KMS; the table's own encryption does not carry over to the files.
How do I check a DynamoDB import for errors?
Run describe-import and read the error count and CloudWatch log group; a completed import can still contain failed rows.
Can I export a DynamoDB table to a bucket in another Region?
Yes; the bucket can be in any Region, with normal inter-Region data transfer charges on top of the export fee.
Is a DynamoDB export cheaper than a scan?
Not always in raw dollars; a one-off scan of an on-demand table can cost less. The export uses no capacity, gives a consistent snapshot and needs no code.
How do I export DynamoDB to CSV with Python?
Read the export's manifest and gzip data files from S3, convert each item with boto3's TypeDeserializer, and write rows with csv.DictWriter.
How do I start a DynamoDB export with boto3?
Call export_table_to_point_in_time with the table ARN and bucket, then poll describe_export until the status leaves IN_PROGRESS.
Can AWS Glue read DynamoDB without consuming capacity?
Yes. Set dynamodb.export to ddb so the job reads from a fresh export instead of scanning the table; PITR must be enabled.
Is an export the same as a DynamoDB backup?
No. Use point-in-time restore or backups to recover a table; exports produce files for analytics and data sharing.
How do I schedule a nightly DynamoDB export?
Use an EventBridge Scheduler schedule that starts a Step Functions state machine or Lambda function calling the export API, then waits for completion.
For readers preparing for an AWS certification
We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.
Exams that test this topic: Data Engineer – Associate • Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03).
Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:
1. A team must export a large DynamoDB table to S3 for analytics without affecting application performance. What should it use?
DynamoDB export to S3 (requires point-in-time recovery; consumes no read capacity).
2. Which feature must be enabled on a DynamoDB table before exporting it to S3?
Point-in-time recovery (PITR).
3. How can only the items changed during the last day be exported?
An incremental export for that time window.
4. Which service can query exported DynamoDB JSON files in S3 with SQL and write Parquet?
Amazon Athena (for example with CTAS).
5. Can DynamoDB import from S3 load data into an existing table?
No; it creates a new table. Existing tables need BatchWriteItem or a Glue job.
6. Does DynamoDB import from S3 consume write capacity?
No.
Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.
Finance got its CSV the same afternoon, analytics got a Parquet table that costs pennies to query, and the live site never noticed. The shop now runs a nightly incremental export of the Orders table into the analytics bucket, and Jake's Scan script is gone.
The finance export uses a projection, so it carries order IDs, totals, dates and status, and no customer details. The export bucket expires raw files after 30 days and allows only the analytics roles. When the team built a staging environment in a second account, the same export fed an import there, with the TTL and alarms set up by hand afterwards. Every piece is managed, none of it touches the table's capacity, and the monthly cost of the whole arrangement is smaller than one evening of the original Scan script slowing the site.
📌 If you keep one line from this page
Turn on PITR and use export-table-to-point-in-time for DynamoDB to S3 (no read capacity, about $0.10 per GB), Athena for CSV or Parquet, import-table for a new table from CSV or JSON (about $0.15 per GB), and a batch_writer script for existing tables.
Incremental exports for nightly changes · filtered exports for subsets · --s3-bucket-owner across accounts.
Revision note. Written October 20, 2026, including filtered exports.