DynamoDB to Postgres migration tools, migrate DynamoDB to PostgreSQL with AWS Glue or Streams, and why DMS cannot
AWS DMS does not read DynamoDB, so every route starts from an export or a stream. Most of them hand PostgreSQL your numbers as text. Nine options compared, with the defaults that decide what lands.
The live demo needs no card, and Starter is $49 a month.
Field mapping auto-plugged · tap a port to rewire
Plug a source port into
Transform on this cable
JSON in
JSON out
5 sample records ready
AWS and PostgreSQL documentation read 28 September 2026
How do you migrate DynamoDB to PostgreSQL?
Export the table to S3 from point-in-time recovery, turn the DynamoDB JSON into typed rows, and load them with COPY. Then replay the changes made since the export from DynamoDB Streams or incremental exports until you cut over. AWS DMS is not an option, because DynamoDB is not one of its sources. The hard part is not moving bytes but deciding types: DynamoDB sends every number as a string, so a migration that does not cast leaves prices and timestamps as text in PostgreSQL.
This page sits under our data migration tools guide. If the other document store you are leaving is MongoDB, the same question is answered on MongoDB to PostgreSQL migration tools.
Eight documented defaults between DynamoDB and PostgreSQL
Each row quotes the vendor's own documentation. The third row is the one we rarely see mentioned: AWS's own example output for the Glue transform that "simplifies" DynamoDB JSON shows numbers leaving it as strings.
| Route and setting | What its documentation says | Why it matters to a buyer | What we would ship |
|---|---|---|---|
| AWS DMS | The supported source list names Oracle, SQL Server, MySQL, MariaDB, PostgreSQL, MongoDB, SAP ASE, IBM Db2, DocumentDB and S3. DynamoDB appears only as a target. | "aws dms dynamodb to postgres" is a common search, and the answer is that the service most teams reach for first cannot read the source at all. | Export to S3, then load |
| Export to S3, DynamoDB JSON | Each line is an Item in "DynamoDB's standard marshalled JSON format", with every value inside a type descriptor and every number written as a string. | Loaded straight into a JSONB column, a price sits at item->'Price'->>'N' and is text. | Unwrap descriptors, cast per attribute |
| Glue simplifyDDBJson | AWS's documented output of the transform shows a number attribute as "updatedAt: string" and a number set as "array, element: string". | The transform removes the descriptors and keeps the numbers as text. A Glue job that writes the frame as-is creates text columns in Postgres. | Explicit casts before the JDBC write |
| Glue ETL connector read rate | dynamodb.throughput.read.percent defaults to 0.5. On an on-demand table Glue "handles the read capacity of the table as 40000". | A Glue scan against a production table competes with the application for read capacity, at half the table by default. | Use the export connector instead |
| Glue export connector | Faster than the ETL connector past 80 GB, needs no split or read-rate tuning, and requires point-in-time recovery on the table. | The right default for a migration, and it fails at the first run if nobody turned on PITR. | Enable PITR before the trial |
| Incremental export | AWS: an item inserted and deleted within the window produces "No output". A delete arrives as Keys only, or Keys plus the old image. Windows run 15 minutes to 24 hours. | A catch-up load that only inserts will never remove a deleted item from Postgres. | Upsert on key, Keys-only rows delete |
| PostgreSQL jsonb | PostgreSQL: jsonb "rejects \u0000 (because that cannot be represented in PostgreSQL's text type)" and "does not preserve the order of object keys". | One item holding a NUL character stops a COPY into a jsonb column, deep into a long load. | Strip or escape NUL before loading |
| Airbyte DynamoDB source | Sync modes are full refresh and incremental append. Replicating deletes is "not supported". Schema discovery samples up to 1,000 items per scan. | During a parallel run, deleted items stay in Postgres and the counts drift apart. | Full attribute inventory, explicit deletes |
The Glue row matters because Glue is the answer most AWS teams land on once they learn DMS is out. The job itself, the connector choice and what a run costs are covered in our guide to AWS Glue for DynamoDB to Postgres, and what it bills.
DynamoDB types in PostgreSQL, and the wrong target a converter picks
DynamoDB has no date type and no fixed scale, and PostgreSQL wants both. The second column is how a value travels, the last is the mistake we see most often.
| DynamoDB type | How it travels | Right PostgreSQL target | Common wrong target |
|---|---|---|---|
| N, whole numbers | {"N":"103"}, a string | bigint, or numeric past 18 digits | text, which sorts "100" before "99" |
| N, money | {"N":"19.99"}, a string | numeric(14,2) or wider, fixed scale | double precision, which rounds cents |
| N, very large | Up to 38 digits, exponent to E+125 | numeric, unconstrained | bigint, which overflows past 9.2 quintillion |
| N, epoch time | Seconds or milliseconds, undeclared | timestamptz via to_timestamp, unit stated | The raw bigint, or the wrong unit |
| S, ISO 8601 date | {"S":"2026-09-28T14:05:00Z"} | timestamptz, cast with a guard | text compared as text |
| S, with a NUL | Legal UTF-8 in DynamoDB | text after stripping, or bytea | text, which cannot store it |
| B, binary | Base64 text | bytea via decode(value, 'base64') | text holding the base64 string |
| SS and NS sets | A JSON array, order not kept | text[] or numeric[], sorted on load | Arrays compared position by position |
| M, map | Nested descriptors, 32 levels deep | Columns for used paths, jsonb for the rest | One column per key, truncated at 63 bytes |
| L, list | A JSON array of descriptors | A child table WITH ORDINALITY | jsonb nobody can join cheaply |
Two rows are specific to PostgreSQL. The character with code zero cannot be stored in any PostgreSQL text column, which is documented plainly, while a DynamoDB string is any UTF-8. And identifiers stop at 63 bytes, so a nested path flattened into a column name can collide with its neighbor. The general Postgres loading options are compared on Postgres ETL tools.
DynamoDB to PostgreSQL migration tools compared
Billing units rather than invented price tags. The three dollar figures were read from the vendors' own pricing pages in September 2026: AWS Glue's $0.44 per DPU-hour in US East, Estuary's $0.50 per GB, and our own $49 Starter plan. The last column says where each one is the wrong buy.
| Tool | Approach | Best for | Billing unit | What to watch |
|---|---|---|---|---|
| AWS DMS | Not available: DynamoDB is not a DMS source | Nobody on this route | Not applicable | Only works in the other direction |
| S3 export plus COPY | Full export, your script flattens, COPY loads | One-time moves of a few tables | AWS per GB exported, plus your time | You write the typing and the replay |
| AWS Glue | Spark job, ETL or export connector, JDBC write | Spark teams already on AWS | $0.44 per DPU-hour, 1-minute minimum | Numbers arrive as strings |
| Lambda on Streams | Your function upserts each change | Keeping Postgres current during cutover | Lambda invocations and your time | 24-hour retention, retries are yours |
| Airbyte | Full refresh or incremental append | Open source teams who can patch it | Free self-hosted, credits on cloud | No deletes; 1,000-item sample |
| Estuary | Continuous capture into Postgres | Low-latency sync with short setup | $0.50 per GB plus connector instances | Ask how the backfill reads the table |
| Hevo | Managed pipeline with flattening | Smaller teams wanting a UI first | Plans by event volume | Ask how deep flattening goes |
| Podyn | Open source replicator, columns or jsonb | Engineers happy to run old code | Free, self-run | Introduced in 2017; check maintenance |
| Adapters | Mapped sync of declared paths to typed columns | Typed Postgres tables on a flat bill | Flat monthly plan from $49 | Not a sub-second streaming platform |
If you are choosing between a one-time move and an ongoing sync, the distinction is drawn on change data capture tools. For the same DynamoDB table feeding a warehouse instead, see the DynamoDB to Snowflake connector comparison.
Six numbers to know before you plan the cutover
0
Times DynamoDB appears in the AWS DMS list of supported source endpoints. It is supported only as a target.
AWS DMS sources, read 28 Sep 2026
38 digits
The precision of a DynamoDB number. PostgreSQL bigint stops at about 9.2 quintillion, double precision keeps at least 15, and unconstrained numeric holds all 38.
AWS data types and PostgreSQL numeric docs
63 bytes
The longest PostgreSQL identifier by default. DynamoDB attribute names can run to 64 KB, so long nested paths get truncated as column names.
PostgreSQL lexical structure docs
0.5
The share of table read capacity a Glue ETL connector scan uses by default. On-demand tables are treated as 40,000 read units.
AWS Glue DynamoDB docs, read 28 Sep 2026
24 hours
How long DynamoDB Streams keeps a change. A cutover consumer that stops for longer loses changes it can never read back.
AWS DynamoDB Streams docs
$0.44
Per DPU-hour for a standard AWS Glue Spark job in US East, billed per second with a 1-minute minimum. Flex runs at $0.29.
AWS Glue pricing, read 28 Sep 2026
Seven ways a DynamoDB to Postgres migration loses data while every check stays green
| The failure | What you see | What is actually happening |
|---|---|---|
| Truncated column names | Load succeeds | Two long attribute paths share their first 63 bytes. PostgreSQL truncates both identifiers to the same name, and a generated CREATE TABLE either fails or a script dedupes by keeping one. |
| Case-folded attributes | Fewer columns than expected | DynamoDB attribute names are case-sensitive. Unquoted identifiers in PostgreSQL fold to lower case, so customerId and customerid collapse into one column unless every name is quoted. |
| Numbers kept as text | Reports render | The export and Glue's simplify transform both hand numbers over as strings. Stored as text, "9" sorts after "10" and MAX() returns the wrong order. |
| Precision lost in double | Totals off by a cent | A generic mapper picks double precision for numbers. DynamoDB kept 38 exact digits, double keeps about 15, and money sums stop tying out to the ledger. |
| Deletes during the parallel run | Row counts only grow | An append-only catch-up never sees a delete. Items removed in DynamoDB while both systems run stay in PostgreSQL and get migrated as live. |
| The weekend gap | Consumer resumes, no error | The Streams consumer was down for more than 24 hours. The oldest changes were trimmed, and Postgres is missing them from the day of cutover onward. |
| The attribute the sample missed | Sync green | Schema discovery read a sample of items. An attribute that only older or rarer items carry never became a column, and it vanishes when DynamoDB is switched off. |
The first two are PostgreSQL naming rules meeting a schemaless source, and neither shows up in a row count. The same class of problem, for a relational source, is laid out on MySQL to PostgreSQL migration tools.
Six steps from a DynamoDB table to a PostgreSQL schema you can switch to
-
1
Inventory every attribute from a full export
Export once with point-in-time recovery and count every attribute path, how often it appears and which type descriptors it carries. DynamoDB enforces only the key, so this is the only complete list of what your PostgreSQL schema has to hold.
-
2
Design tables from the access patterns
Single-table designs keep customers, orders and invoices under key prefixes in one table. Split them into one PostgreSQL table per entity, with foreign keys, and keep the original partition and sort key as a unique constraint for the parallel run.
-
3
Declare types and the jsonb fallback
Numeric scale for money, the unit for every epoch field, arrays for sets, child tables for lists. Every item also lands whole in a jsonb column with NUL characters removed, so an attribute you missed can still be recovered after cutover.
-
4
Backfill from the export, not a scan
Load history from the S3 export with COPY. It uses no table read capacity. Note the export time: that is the point every later change has to be replayed from.
-
5
Replay changes until cutover
Apply DynamoDB Streams or incremental exports with INSERT ON CONFLICT, and treat Keys-only records as deletes. Alert well inside the 24-hour Streams retention, because a gap here means another full backfill.
-
6
Reconcile, then switch writes
Compare item counts from the export manifest with rows per table, sums of money columns, and deletes seen against rows removed. Switch the application to PostgreSQL only when all three agree for several days.
Why US teams move DynamoDB workloads to PostgreSQL
Queries DynamoDB was never built for
The product needs joins, ad hoc filters and aggregates that a key-value design answers only with a new index per question. PostgreSQL answers them with SQL, and the team wants the move done without losing a record.
Finance tables that have to tie out
Payments and subscriptions live in items with nested maps. Finance needs numeric columns with a fixed scale and real foreign keys to customers, not strings cast in every query.
Consolidating onto Aurora or RDS
Most of the stack already runs on Aurora PostgreSQL. One or two services on DynamoDB are the exception, and the team wants one database to operate, back up and audit.
Multi-cloud or leaving AWS
DynamoDB exists only on AWS. PostgreSQL runs on every cloud and on premises, which matters to buyers who need a portable data layer or a customer-hosted edition.
Splitting a single-table design
One table holds six entity types under different key prefixes. Engineers joining the team cannot read it, and the migration is the moment to give each entity its own table.
Keeping both during a long cutover
The application moves service by service over a quarter. PostgreSQL has to stay current with DynamoDB the whole time, deletes included, so every service can switch when it is ready.
Five migrations where you should not pick us
- You are moving one small table once and have an engineer for a day. An S3 export, a short script and COPY cost almost nothing and leave nothing to maintain.
- Your team writes Spark on AWS every day. A Glue job with explicit casts and the export connector is a well-documented AWS path, and you already know how to run it.
- You need sub-second latency from a DynamoDB write to a PostgreSQL row for an operational feature. A streaming platform built for that beats our incremental sync.
- The target is Redshift rather than PostgreSQL. AWS offers a DynamoDB zero-ETL integration with Redshift that needs no pipeline at all.
- Nobody has decided which attributes matter yet. Land the raw items in jsonb with any tool here, let the queries settle, and type the columns afterwards.
Four things to ask any migration vendor
Question 01
Show me the CREATE TABLE after a test load
One jsonb column, first-level text columns, or typed columns and child tables are all possible answers. Ask for the DDL of a real table from your own data before you compare prices.
Question 02
How does the backfill read our table?
A scan uses read capacity your application also needs. An S3 export does not. Ask which one the tool uses, and what it does to a table in on-demand mode.
Question 03
How are deletes carried during the parallel run?
Removed, flagged, or never seen. A tool that cannot see deletes is fine for a one-shot copy and wrong for a cutover that runs for weeks.
Question 04
What happens to an attribute you have not seen?
Dropped, added as a new column, or kept in a jsonb fallback. The answer decides whether a field written by an old app version survives the move.
Questions buyers ask about DynamoDB to PostgreSQL
- Can AWS DMS migrate DynamoDB to PostgreSQL?
- No. AWS DMS supports DynamoDB only as a target. Its list of supported source endpoints covers Oracle, SQL Server, MySQL, MariaDB, PostgreSQL, MongoDB, SAP ASE, IBM Db2, DocumentDB and Amazon S3, and DynamoDB is not on it. The AWS-native route is a DynamoDB export to S3, read by AWS Glue or a loader of your own.
- How do I migrate data from DynamoDB to PostgreSQL?
- Export the table to S3 from point-in-time recovery, convert the DynamoDB JSON into typed rows, and load them into PostgreSQL with COPY. Then replay the changes made during the load from DynamoDB Streams or an incremental export, and cut over. AWS Glue, Airbyte, Estuary and Hevo automate parts of this, each with different defaults.
- Can AWS Glue move DynamoDB to Postgres?
- Yes. A Glue Spark job reads DynamoDB through its ETL connector or its export connector and writes to PostgreSQL over JDBC. The export connector needs point-in-time recovery and does not scan the table. The ETL connector scans it at half the table's read capacity by default. Either way the typed schema is code you write.
- Should DynamoDB items become PostgreSQL columns or JSONB?
- Both. Put the attributes your queries filter, join and sum on into typed columns, and keep the full item in a JSONB column so nothing is lost when the application adds an attribute. JSONB alone makes every report cast text, and columns alone drop whatever the migration script did not know about.
- How do I stream DynamoDB changes to Postgres?
- Enable DynamoDB Streams with new and old images and run a consumer, usually AWS Lambda, that upserts each change into PostgreSQL with INSERT ON CONFLICT on the primary key. Streams keeps each change once, in order per item, for 24 hours, so a consumer that stops for longer than a day needs a reload from an export.
- How long does a DynamoDB to PostgreSQL migration take?
- The data copy is rarely the long part. A full export runs asynchronously and AWS gives no fixed duration, and COPY into PostgreSQL is fast. The weeks go into deciding the relational schema for a schemaless table, rewriting the access patterns built around one table, and running both databases in parallel until the numbers agree.
- How much does it cost to migrate DynamoDB to PostgreSQL?
- Three lines. AWS bills a full export by table size and an incremental export by data processed, with a 10 MB minimum. The mover bills in its own unit: $0.44 per DPU-hour for a standard AWS Glue job in US East, credits at Airbyte Cloud, $0.50 per GB at Estuary. Engineering time for the schema is usually the largest line.
- Why do DynamoDB numbers land in Postgres as text?
- Because DynamoDB sends every number as a string, and the export keeps it inside a type descriptor such as {"N":"600"}. AWS Glue's own simplify transform documents number attributes and number sets coming out as strings. Unless the load casts them, PostgreSQL stores text, and sorting or MAX() then works on characters.
- What is the best tool to migrate DynamoDB to PostgreSQL?
- For a one-time move of a few tables, an S3 export plus a load script is cheapest. For a Spark team on AWS, Glue. For ongoing sync with no pipeline to run, Estuary or Hevo. When you need typed columns and child tables from nested items on a flat monthly bill, a mapped sync like Adapters.
Related migration and PostgreSQL guides
AWS Glue DynamoDB to Postgres
The job, the connector choice, what a run costs, and why DMS is out.
Data migration tools
One-time cutovers from any database, and the failures that report success.
MongoDB to PostgreSQL migration tools
The other document store, with the tables or JSONB decision.
Postgres ETL tools
Every way to load PostgreSQL from databases and SaaS apps.
DynamoDB to Snowflake connector
The same table into a warehouse, and what each connector lands.
Change data capture tools
Streams against binlogs and logical replication, compared.
Move DynamoDB to PostgreSQL as typed tables
Declare the attributes that matter, split single-table designs by entity, send lists to child tables, backfill from an export, then keep Postgres current with deletes until you cut over. From $49 a month, not metered by rows.
The live demo needs no card, and Starter is $49 a month.