[Update] Amazon Aurora DSQL Change Data Capture is now generally available
This page has been translated by machine translation. View original
I'm Oguri, who loves whiskey, cigars, and pipes.
Amazon Aurora DSQL's Change Data Capture (CDC), announced as a preview in May 2026, became generally available (GA) on July 8, 2026! The "INSERT and UPDATE cannot be distinguished" issue, which was the biggest concern during the preview, has also been resolved, so I'll introduce the changes as the main focus.
For the content from the preview period, please see the following entry.
What is Aurora DSQL CDC
Aurora DSQL's CDC is a feature that delivers committed database changes to Amazon Kinesis Data Streams in near real-time. It automatically captures row-level results of INSERT, UPDATE, and DELETE operations on all user tables in the cluster, and delivers them as structured JSON records.
The main features are as follows.
| Item | Details |
|---|---|
| Delivery destination | Amazon Kinesis Data Streams (BYOT: Bring Your Own Target model) |
| Target | Row-level changes for all user tables in the cluster |
| Record format | Structured JSON |
| Delivery guarantee | At-least-once. The same record may be delivered multiple times |
| Order | UNORDERED (delivered in an order close to commit order, but no strict ordering guarantee) |
| Multi-region | Captures committed DSQL writes from all regions with a single stream in one region |
| Performance impact | No impact on database workload performance |
The CDC stream itself is fully managed, and the infrastructure needed to capture change events is managed by Aurora DSQL. Users prepare the destination Kinesis data stream and the IAM role that Aurora DSQL uses for writing in their own account.
The following use cases are mentioned.
- Downstream system synchronization: Replication of changes to search indexes, caches, data warehouses, and analytics systems.
- Event-driven architecture: Triggering workflows, notifications, and microservices based on database changes.
- Audit trail: Recording transactions for compliance, debugging, and historical analysis.
- Decoupling producers and consumers: The database focuses on transactions, and downstream processes changes at its own pace. Applications such as CQRS.
After records arrive in Kinesis Data Streams, they can be used as an event source for AWS Lambda, or delivered to Amazon S3, Amazon Redshift, or Amazon OpenSearch Service via Amazon Data Firehose.
Changes from the Preview
The biggest change with GA is that UPDATE is now distinguished in the op field of CDC records.
| Item | At preview | At GA |
|---|---|---|
INSERT op |
"c" |
"c" |
UPDATE op |
"c" (indistinguishable from INSERT) |
"u" |
DELETE op |
"d" |
"d" |
UPDATE is now distinguished with op: "u"
At the preview stage, both INSERT and UPDATE were delivered as op: "c", making it impossible for the downstream side to distinguish them. As announced, op: "u" has been added in GA, making it possible to identify the type of row change.
An example UPDATE record looks like this.
{
"type": "full",
"op": "u",
"before": null,
"after": {"order_id": 1001, "item_id": 42, "quantity": 10, "price": "29.99"},
"source": {
"version": "1.0",
"ts_ms": 1705318300000,
"ts_ns": 1705318300000000000,
"txId": "qvtiesgmd55cvlfukm3dfuotji",
"schema": "public",
"table": "order_items",
"db": "postgres",
"cluster": "kmabugltfmjdaj2siqr2qbxgju"
},
"ts_ms": 1705318300125,
"ts_ns": 1705318300125483291
}
The after field contains all columns of the row after the change. It remains unchanged that the before image of the row is not included even for UPDATE, and before is null. Note that for DELETE, if the table has a primary key, the before field will contain the primary key value of the deleted row.
Write-set compaction
In the GA documentation, how multiple operations within a transaction are combined into one record has been formalized as "write-set compaction." Aurora DSQL compares the before and after states of each row at transaction commit time, and emits at most one record per row for the net effect.
| Row before transaction | Row after transaction | Emitted record |
|---|---|---|
| Does not exist | Exists | op: "c" (after is the final state) |
| Exists | Exists | op: "u" (after is the final state) |
| Exists | Does not exist | op: "d" (before contains only the primary key value) |
| Does not exist | Does not exist | No record |
For example, if a row is INSERTed and then UPDATEd within the same transaction, only one op: "c" record is emitted. If an INSERTed row is DELETEd, no record is emitted because there is no net change. If you need per-SQL-statement-level recording, such as for audit logs, you need to separate each statement into its own transaction.
Notes
The following points require attention even at GA.
- Delivery guarantee is at-least-once: The same record may be delivered multiple times. Duplicates can be detected using the combination of
source.ts_ns(commit timestamp with nanosecond precision) and primary key values. - Order is UNORDERED: Records are delivered roughly in commit order, but there is no strict guarantee. If commit order is required, use
source.ts_nson the receiving side to sort records. Usesource.txIdto group records from the same transaction. - Primary key definition is required: Without a primary key on a table, the downstream cannot perform deduplication or identify the target row for deletion. Always define a primary key on tables subject to CDC.
- Kinesis-side settings: Set
MaxRecordSizeInKiBto 10240 (10 MiB) instead of the default 1 MiB. If a record exceeding the configured limit occurs, the CDC stream will become impaired withKINESIS_OVERSIZE_RECORD. - Records larger than 9 MiB are split: If the serialized JSON exceeds 9 MiB, it is split into
chunked/fragmentrecords, requiring reassembly on the receiving side. - Stream count limit: There is a limit on the number of CDC streams per cluster. When the limit is reached,
CreateStreamreturnsServiceQuotaExceededException. Check the quota documentation for default values.
Pricing
CDC pricing is measured as StreamDPU based on the amount of data captured. StreamDPU is charged at the same rate as other DPU subcomponents.
| Item | Details |
|---|---|
| CDC billing unit | StreamDPU (same rate as regular DPU) |
| DPU unit price | $10.00 / 1 million DPU (for Asia Pacific (Tokyo)) |
| When there are no changes | No Stream DPU charges |
| Kinesis Data Streams | Standard charges apply separately |
| Free tier | 100,000 DPU per month + 1 GB-month of storage |
Since Stream DPU charges do not occur when there are no changes to stream, you don't need to worry about the cost of simply having a CDC stream created. Note that charges for Kinesis Data Streams and downstream services such as Lambda and Firehose apply separately.
For the latest pricing, please check the Aurora DSQL pricing page.
Let's Try It
With the GA version of the CDC stream, let's verify that the op field is distinguished as c / u / d, and confirm the behavior of write-set compaction.
The prerequisites are as follows.
- The Aurora DSQL cluster must be in
ACTIVEstatus (if it is in an idle state, connect with a PostgreSQL client to wake it up.CreateStreamwill result in a validation error if the cluster is notACTIVE). - The Aurora DSQL cluster, Kinesis data stream, IAM role, and calling principal must all be in the same AWS account and same region.
- CLI is executed on CloudShell.
Setting Up the CDC Stream
The setup procedure is the same as during the preview. For detailed IAM policy content, please refer to the preview entry. Here we only recap the key points.
First, create the destination Kinesis data stream. The record size limit (MaxRecordSizeInKiB) is recommended to be 10240 (10 MiB), and on-demand is recommended for the capacity mode.
$ aws kinesis create-stream \
--stream-name my-cdc-stream \
--stream-mode-details StreamMode=ON_DEMAND \
--max-record-size-in-ki-b 10240 \
--region ap-northeast-1
Next, create an IAM role for Aurora DSQL to write to Kinesis. Set dsql.amazonaws.com as the Principal in the trust policy, and configure aws:SourceAccount and aws:SourceArn conditions as confused deputy countermeasures. Grant kinesis:PutRecord / kinesis:PutRecords / kinesis:DescribeStreamSummary / kinesis:ListShards in the permissions policy (if using a customer-managed KMS key, kms:GenerateDataKey is also required).
Create the CDC stream.
$ aws dsql create-stream \
--cluster-identifier CLUSTER_ID \
--target-definition '{"kinesis":{"streamArn":"arn:aws:kinesis:ap-northeast-1:123456789012:stream/my-cdc-stream","roleArn":"arn:aws:iam::123456789012:role/dsql-cdc-role"}}' \
--ordering UNORDERED \
--format JSON \
--region ap-northeast-1
{
"clusterIdentifier": "CLUSTER_ID",
"streamIdentifier": "STREAM_ID",
"arn": "arn:aws:dsql:ap-northeast-1:123456789012:cluster/CLUSTER_ID/stream/STREAM_ID",
"status": "CREATING",
"creationTime": "2026-07-09T02:47:15.854000+00:00",
"ordering": "UNORDERED",
"format": "JSON"
}
Stream creation takes approximately 1 to 3 minutes. Use get-stream to confirm that the status has become ACTIVE.
$ aws dsql get-stream \
--cluster-identifier CLUSTER_ID \
--stream-identifier STREAM_ID \
--region ap-northeast-1 \
--query 'status'
"ACTIVE"
Verifying op with INSERT / UPDATE / DELETE
Create a test table and execute INSERT, UPDATE, and DELETE each in separate transactions (auto-commit). To perform deduplication and deletion correlation with CDC, always define a primary key on the table.
CREATE TABLE test_cdc (
id INT PRIMARY KEY,
message TEXT
);
INSERT INTO test_cdc VALUES (1, 'hello cdc');
UPDATE test_cdc SET message = 'hello ga' WHERE id = 1;
DELETE FROM test_cdc WHERE id = 1;
Read records from the Kinesis data stream. Since Aurora DSQL distributes records across shards using a random partition key, all shards must be read.
$ for SHARD_ID in $(aws kinesis list-shards \
--stream-name my-cdc-stream \
--region ap-northeast-1 \
--query 'Shards[].ShardId' --output text); do
SHARD_ITERATOR=$(aws kinesis get-shard-iterator \
--stream-name my-cdc-stream \
--shard-id "$SHARD_ID" \
--shard-iterator-type TRIM_HORIZON \
--region ap-northeast-1 \
--query 'ShardIterator' --output text)
aws kinesis get-records \
--shard-iterator "$SHARD_ITERATOR" \
--region ap-northeast-1 \
--query 'Records[].Data' --output text
done
Decoding the retrieved Data fields (Base64 encoded) reveals records corresponding to the three operations.
INSERT is delivered with op: "c".
{
"type": "full",
"op": "c",
"before": null,
"after": {
"id": 1,
"message": "hello cdc"
},
"source": {
"version": "1.0",
"ts_ms": 1783565823773,
"ts_ns": 1783565823773460473,
"txId": "svt5cm5lx2xbhzbn77gzxmp7ci",
"schema": "public",
"table": "test_cdc",
"db": "postgres",
"cluster": "CLUSTER_ID"
},
"ts_ms": 1783565823772,
"ts_ns": 1783565823772881451
}
UPDATE is delivered with op: "u". During the preview it was "c", the same as INSERT, so it is indeed now distinguishable!
{
"type": "full",
"op": "u",
"before": null,
"after": {
"id": 1,
"message": "hello ga"
},
"source": {
"version": "1.0",
"ts_ms": 1783565823840,
"ts_ns": 1783565823840086440,
"txId": "xbt5cm5l2fkdumfhdg6w77rzom",
"schema": "public",
"table": "test_cdc",
"db": "postgres",
"cluster": "CLUSTER_ID"
},
"ts_ms": 1783565823839,
"ts_ns": 1783565823839891247
}
DELETE is delivered with op: "d", and the before field contains the primary key value.
{
"type": "full",
"op": "d",
"before": {
"id": 1
},
"after": null,
"source": {
"version": "1.0",
"ts_ms": 1783565823880,
"ts_ns": 1783565823880754862,
"txId": "yft5cm5l3rkt2k5fa25tjalc6i",
"schema": "public",
"table": "test_cdc",
"db": "postgres",
"cluster": "CLUSTER_ID"
},
"ts_ms": 1783565823880,
"ts_ns": 1783565823880136762
}
Verifying Write-set Compaction
Next, let's verify that multiple operations within the same transaction are compacted into one record. INSERT a row and then UPDATE it within the same transaction before committing.
BEGIN;
INSERT INTO test_cdc VALUES (2, 'first');
UPDATE test_cdc SET message = 'compacted' WHERE id = 2;
COMMIT;
Only one record with op: "c" is delivered, and after contains the final state after the UPDATE.
{
"type": "full",
"op": "c",
"before": null,
"after": {
"id": 2,
"message": "compacted"
},
"source": {
"version": "1.0",
"ts_ms": 1783566449655,
"ts_ns": 1783566449655623084,
"txId": "mvt5cnqowtxtj75hju7kwhwa7m",
"schema": "public",
"table": "test_cdc",
"db": "postgres",
"cluster": "CLUSTER_ID"
},
"ts_ms": 1783566449566,
"ts_ns": 1783566449566168084
}
Furthermore, what happens when an INSERTed row is DELETEd within the same transaction?
BEGIN;
INSERT INTO test_cdc VALUES (3, 'ephemeral');
DELETE FROM test_cdc WHERE id = 3;
COMMIT;
Since the net change to the row is zero, no record for id = 3 is delivered. As stated in the documentation, it is confirmed that CDC delivers the net result of a transaction, not per-statement results.
Conclusion
It became GA approximately two months after the preview announcement. The issue of INSERT and UPDATE both being op: "c", which was the biggest concern during the preview, was resolved as announced, and the behavior of write-set compaction was also formally documented, making it possible to confidently design downstream processing.
Aurora DSQL is a serverless, strongly consistent distributed SQL database, but with the GA of CDC, it can now also be adopted in production as the starting point for event-driven architectures. Since the at-least-once and UNORDERED characteristics remain unchanged even at GA, as long as you incorporate deduplication and ordering by source.ts_ns and primary key into the downstream side, I'd like to make full use of it for typical use cases such as search index synchronization and audit trails.
