DMS Task Stuck or Failing During CDC
-
Phase 0: Before you start
-
Phase 1: The foundation
-
Phase 2: Operations and security
-
Phase 3: SQL Server on AWS
-
Phase 4: Troubleshooting step by step
-
Phase 5: Final project and career
A DMS task that fails loudly is the easy case. The hard ones stall after a weekend, or keep “running” while quietly dropping changes they couldn’t apply. Fixing an AWS DMS task failed during CDC means reading three things: the task’s failure message, its table statistics, and the apply exceptions table on the target.
You’ll run a full-load-and-cdc task from SaatShop to a replica database on onprem-sql, then break it twice the way production breaks: log backups deleted while the task was stopped, and a row written to the target by hand. You’ll diagnose and fix both.
| Time | You need | By the end you can |
|---|---|---|
| 120 minutes | Lessons 47 to 50 and 56 done (onprem-sql stopped with SaatShop, dms-vpc-role and dms-cloudwatch-logs-role, Start-SsmEndpoints.ps1), signed in as saat-admin. Cost: about USD 2 (onprem-sql’s SQL Server Standard license for about two hours plus a small replication instance). To keep it cheap, the target is a second database on onprem-sql itself. | Monitor CDC tasks with status, LastFailureMessage and table statistics, understand log truncation on SQL Server sources, read awsdms_apply_exceptions, and recover with reload-target and reload-tables. |
What it is, in one line
An AWS DMS task failed during CDC when it stops with an error, usually because it lost its place in the source transaction log, or when it keeps running but logs changes it couldn’t apply to the target.
In Simple Words
The copy clerk and the diary
A clerk copies every new line from the boss’s diary into a second book. One Friday the clerk goes home. Over the weekend, the boss tears out old pages to save space, including some the clerk never copied.
On Monday the clerk opens the diary and can’t find where they stopped. The only safe fix is to copy the whole diary again and carry on from today.
Another day, someone scribbles straight into the second book, so the real line won’t fit in its spot. A careful clerk notes it on a “couldn’t copy” list instead of stopping work. Somebody has to read that list.
Engineer's View
How CDC tasks fail on SQL Server
DMS reads committed changes from the source transaction log, and from log backups if the online log was truncated, as long as those backups are still readable on the server. While a task runs, its safeguard settings (safeguardPolicy) hold the log open; when it’s stopped, normal log backups can truncate the part DMS hasn’t read. If those backups are deleted or moved, resuming fails with an error that the LSN can’t be found in the backup log sets.
The fix is a reload; prevention is not leaving tasks stopped and keeping log backups readable until DMS is past them.
Apply errors are handled by task settings. By default a failed insert or update is logged, not fatal: the task keeps running and writes the statement and error to awsdms_apply_exceptions on the target. Table statistics show inserts, updates, deletes and the table state; reload-tables reloads one table without restarting everything.
The task’s Status, StopReason and LastFailureMessage describe fatal errors, and the CloudWatch task log has the detail. CDCLatencySource and CDCLatencyTarget show whether it’s keeping up.
How it works, step by step
Here is the order to check a misbehaving CDC task.
1Task status and failure message
Failed or stopped? LastFailureMessage often names the exact problem, such as missing log backups or a login failure.
2Table statistics
Which tables are in error, and do insert counts look right? A table that stopped counting while others continue is a clue.
3Apply exceptions on the target
Even a “running” task can be dropping changes. Query awsdms_apply_exceptions after every incident and on a schedule.
4Match the sign to the fix
The usual suspects:
Row counts that match today prove little. Compare counts and exceptions, or turn on validation (Lesson 48), before trusting a CDC target.
✅ Checkpoint: You can explain why stopped tasks plus log backups lose CDC position, and where DMS records changes it couldn’t apply.
Hands-on lab: break a CDC task twice and recover it
Sign in, start onprem-sql and the endpoints, and add the helpers. Invoke-Sql runs T-SQL on onprem-sql; Get-DmsHealth shows the task, its tables and both row counts:
cd C:\aws-lab
aws login --profile saat-admin
$env:AWS_PROFILE = 'saat-admin'
$v = Get-Content C:\aws-lab\saat-vpc.json | ConvertFrom-Json
.\Start-SsmEndpoints.ps1
$src = aws ec2 describe-instances --filters "Name=tag:Name,Values=onprem-sql" "Name=instance-state-name,Values=stopped,running" --query "Reservations[0].Instances[0].InstanceId" --output text
$srcIp = aws ec2 describe-instances --instance-ids $src --query "Reservations[0].Instances[0].PrivateIpAddress" --output text
$srcSg = aws ec2 describe-security-groups --filters "Name=group-name,Values=saat-onprem-sql-sg" --query "SecurityGroups[0].GroupId" --output text
aws ec2 start-instances --instance-ids $src | Out-Null
do { Start-Sleep 20; $p = aws ssm describe-instance-information --filters "Key=InstanceIds,Values=$src" --query "InstanceInformationList[0].PingStatus" --output text } until ($p -eq 'Online')
function Invoke-Ps([string]$Line) {
@{ commands = @($Line) } | ConvertTo-Json | Set-Content src-cmd.json
$cmd = aws ssm send-command --document-name AWS-RunPowerShellScript --instance-ids $src --parameters file://src-cmd.json --query Command.CommandId --output text
do { Start-Sleep 5; $st = aws ssm get-command-invocation --command-id $cmd --instance-id $src --query Status --output text 2>$null } until ($st -in 'Success', 'Failed', 'TimedOut')
(aws ssm get-command-invocation --command-id $cmd --instance-id $src --query StandardOutputContent --output text).Trim()
}
function Invoke-Sql([string]$Database, [string]$Sql) { Invoke-Ps ('sqlcmd -S . -E -C -h -1 -W -s " " -d ' + $Database + ' -Q "SET NOCOUNT ON; ' + $Sql + '"') }
function Get-DmsHealth {
aws dms describe-replication-tasks --filters "Name=replication-task-arn,Values=$task" --query "ReplicationTasks[0].[Status,StopReason,LastFailureMessage]" --output text
aws dms describe-table-statistics --replication-task-arn $task --query "TableStatistics[].[TableName,TableState,Inserts,AppliedInserts]" --output text
"source $(Invoke-Sql SaatShop 'SELECT COUNT(*) FROM dbo.Orders;') target $(Invoke-Sql SaatShopReplica 'SELECT COUNT(*) FROM dbo.Orders;')"
}
Invoke-Sql SaatShop 'SELECT COUNT(*) FROM dbo.Orders;'You should see something like this:
503721Prepare the source and the replica database
Paste this whole block. It sets a new password for dms_user, turns on FULL recovery and MS-CDC, and creates the target database:
$dmsPw = aws secretsmanager get-random-password --password-length 20 --exclude-punctuation --require-each-included-type --query RandomPassword --output text
Invoke-Sql master "ALTER LOGIN dms_user WITH PASSWORD = '$dmsPw'; ALTER SERVER ROLE sysadmin ADD MEMBER dms_user; CREATE DATABASE SaatShopReplica;" | Out-Null
Invoke-Sql SaatShop "ALTER DATABASE SaatShop SET RECOVERY FULL; BACKUP DATABASE SaatShop TO DISK = 'C:\sqlbak\dms64-full.bak' WITH INIT; EXEC sys.sp_cdc_enable_db;" | Out-Null
foreach ($t in 'Orders', 'Customers', 'OrderAudit') { Invoke-Sql SaatShop "EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'$t', @role_name = NULL, @supports_net_changes = 1;" | Out-Null }
Invoke-Sql SaatShop "SELECT name + ' ' + CAST(is_tracked_by_cdc AS varchar(1)) FROM sys.tables WHERE schema_id = SCHEMA_ID('dbo') ORDER BY name;"You should see something like this:
Customers 1
OrderAudit 1
Orders 1All three tables are tracked, the log chain has started, and SaatShopReplica waits as the target. Using the same server for both keeps the bill down; DMS treats them as two separate endpoints.
2Start the replication instance and endpoints
Paste this whole block (about 15 minutes):
$dmsSg = aws ec2 create-security-group --group-name saat-dms-sg --description "DMS replication instance" --vpc-id $v.VpcId --query GroupId --output text
aws ec2 authorize-security-group-ingress --group-id $srcSg --ip-permissions "IpProtocol=tcp,FromPort=1433,ToPort=1433,UserIdGroupPairs=[{GroupId=$dmsSg,Description=1433-from-dms}]" | Out-Null
aws dms create-replication-subnet-group --replication-subnet-group-identifier saat-dms-subnets --replication-subnet-group-description "saat-vpc private subnets" --subnet-ids $v.PrivateA $v.PrivateB | Out-Null
$ri = aws dms create-replication-instance --replication-instance-identifier saat-dms --replication-instance-class dms.t3.medium --allocated-storage 20 --replication-subnet-group-identifier saat-dms-subnets --vpc-security-group-ids $dmsSg --no-publicly-accessible --no-multi-az --tags Key=course,Value=saatvedha-aws Key=lesson,Value=64 --query ReplicationInstance.ReplicationInstanceArn --output text
aws dms wait replication-instance-available --filters "Name=replication-instance-arn,Values=$ri"
$srcEp = aws dms create-endpoint --endpoint-identifier saat-shop-src --endpoint-type source --engine-name sqlserver --server-name $srcIp --port 1433 --database-name SaatShop --username dms_user --password $dmsPw --query Endpoint.EndpointArn --output text
$tgtEp = aws dms create-endpoint --endpoint-identifier saat-replica-tgt --endpoint-type target --engine-name sqlserver --server-name $srcIp --port 1433 --database-name SaatShopReplica --username dms_user --password $dmsPw --query Endpoint.EndpointArn --output text
foreach ($ep in $srcEp, $tgtEp) { aws dms test-connection --replication-instance-arn $ri --endpoint-arn $ep | Out-Null }
foreach ($ep in $srcEp, $tgtEp) { aws dms wait test-connection-succeeds --filters "Name=endpoint-arn,Values=$ep" }
aws dms describe-connections --filters "Name=replication-instance-arn,Values=$ri" --query "Connections[].[EndpointIdentifier,Status]" --output textYou should see something like this:
saat-shop-src successful
saat-replica-tgt successful
3Run full load and CDC, and check its health
Paste this whole block. The mapping excludes DMS’s own control tables:
'{ "rules": [ { "rule-type": "selection", "rule-id": "1", "rule-name": "dbo-all", "object-locator": { "schema-name": "dbo", "table-name": "%" }, "rule-action": "include" }, { "rule-type": "selection", "rule-id": "2", "rule-name": "no-dms-tables", "object-locator": { "schema-name": "dbo", "table-name": "awsdms%" }, "rule-action": "exclude" } ] }' | Set-Content mappings64.json
'{ "Logging": { "EnableLogging": true }, "FullLoadSettings": { "TargetTablePrepMode": "DROP_AND_CREATE" } }' | Set-Content task64.json
$task = aws dms create-replication-task --replication-task-identifier saat-shop-cdc64 --source-endpoint-arn $srcEp --target-endpoint-arn $tgtEp --replication-instance-arn $ri --migration-type full-load-and-cdc --table-mappings file://mappings64.json --replication-task-settings file://task64.json --query ReplicationTask.ReplicationTaskArn --output text
aws dms wait replication-task-ready --filters "Name=replication-task-arn,Values=$task"
aws dms start-replication-task --replication-task-arn $task --start-replication-task-type start-replication | Out-Null
aws dms wait replication-task-running --filters "Name=replication-task-arn,Values=$task"
do { Start-Sleep 30; $ts = aws dms describe-table-statistics --replication-task-arn $task --query "TableStatistics[?TableName=='Orders'].TableState | [0]" --output text } until ($ts -eq 'Table completed')
Invoke-Sql SaatShop "INSERT dbo.Orders (Customer, Amount, CustomerId) SELECT TOP (25) CONCAT(N'CDC ', ROW_NUMBER() OVER (ORDER BY (SELECT NULL))), 99.00, 1 FROM sys.all_objects;" | Out-Null
Start-Sleep 60
Get-DmsHealthYou should see something like this:
running None None
Orders Table completed 25 25
Customers Table completed 0 0
OrderAudit Table completed 0 0
source 50397 target 50397The task is running with no failure message, the 25 new orders were captured and applied, and the counts match. This is the picture you want to see every morning.

✅ Checkpoint: Both endpoints test successful, the task is running after full load, Orders shows 25 inserts applied, and source and target both have 50,397 orders.
Break It, Then Fix It
Two classic CDC failures, one loud and one quiet.
1Break it: log backups deleted while the task was stopped
Stop the task as if for a weekend, make changes, take log backups and delete them as an aggressive clean-up job might, then resume:
aws dms stop-replication-task --replication-task-arn $task | Out-Null
aws dms wait replication-task-stopped --filters "Name=replication-task-arn,Values=$task"
foreach ($n in 1..3) {
Invoke-Sql SaatShop "INSERT dbo.Orders (Customer, Amount, CustomerId) SELECT TOP (500) N'Weekend', 10.00, 2 FROM sys.all_objects; CHECKPOINT; BACKUP LOG SaatShop TO DISK = 'C:\sqlbak\weekend$n.trn' WITH INIT;" | Out-Null
Invoke-Ps "Remove-Item C:\sqlbak\weekend$n.trn" | Out-Null
}
aws dms start-replication-task --replication-task-arn $task --start-replication-task-type resume-processing | Out-Null
Start-Sleep 120
aws dms describe-replication-tasks --filters "Name=replication-task-arn,Values=$task" --query "ReplicationTasks[0].[Status,LastFailureMessage]" --output textYou’ll get something like this:
failed Last Error Failed to access LSN '000000a4:00003c10:0001' in the backup log sets since BACKUP/LOG-s are not available. Task error notification received from subtask 0, thread 0 [reptask/replicationtask.c:2883] [1020465]In real life, a backup job that moves .trn files to tape or deletes them after an hour does exactly this. Fix: the place in the log is gone for good, so reload the target and continue from now:
aws dms start-replication-task --replication-task-arn $task --start-replication-task-type reload-target | Out-Null
aws dms wait replication-task-running --filters "Name=replication-task-arn,Values=$task"
do { Start-Sleep 30; $ts = aws dms describe-table-statistics --replication-task-arn $task --query "TableStatistics[?TableName=='Orders'].TableState | [0]" --output text } until ($ts -eq 'Table completed')
Get-DmsHealthYou should see something like this:
running None None
Orders Table completed 0 0
Customers Table completed 0 0
OrderAudit Table completed 0 0
source 51897 target 51897The reload brought in all 1,500 weekend orders. To prevent it: don’t leave CDC tasks stopped for long, keep log backups on the server until DMS has read past them, and alert when a task leaves the running state.
2Break it: someone writes to the target
Insert a row into the replica with the ID the source will use next, then add an order on the source:
$next = Invoke-Sql SaatShop "SELECT CAST(IDENT_CURRENT('dbo.Orders') + 1 AS varchar(20));"
Invoke-Sql SaatShopReplica "INSERT dbo.Orders (OrderId, Customer, Amount, CustomerId) VALUES ($next, N'Manual test', 1.00, 1);" | Out-Null
Invoke-Sql SaatShop "INSERT dbo.Orders (Customer, Amount, CustomerId) VALUES (N'Real order', 4500.00, 3);" | Out-Null
Start-Sleep 90
aws dms describe-replication-tasks --filters "Name=replication-task-arn,Values=$task" --query "ReplicationTasks[0].Status" --output text
Invoke-Sql SaatShopReplica "SELECT TOP 1 TABLE_NAME + ': ' + LEFT(ERROR, 120) FROM dbo.awsdms_apply_exceptions ORDER BY ERROR_TIME DESC;"
Invoke-Sql SaatShopReplica "SELECT Customer FROM dbo.Orders WHERE OrderId = $next;"You’ll get something like this:
running
Orders: RetCode: SQL_ERROR SqlState: 23000 NativeError: 2627 Message: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Violation of PRIMARY KEY
Manual test
The task still says running, but the real order never reached the target; the test row sits in its place. Only the apply exceptions table tells you.
Teams that never query it can run for weeks with silently different data. Fix: remove the stray row and reload the table from the source:
Invoke-Sql SaatShopReplica "DELETE dbo.Orders WHERE OrderId = $next;" | Out-Null
aws dms reload-tables --replication-task-arn $task --tables-to-reload SchemaName=dbo,TableName=Orders --reload-option data-reload | Out-Null
Start-Sleep 30
do { Start-Sleep 20; $ts = aws dms describe-table-statistics --replication-task-arn $task --query "TableStatistics[?TableName=='Orders'].TableState | [0]" --output text } until ($ts -eq 'Table completed')
Invoke-Sql SaatShopReplica "SELECT Customer FROM dbo.Orders WHERE OrderId = $next;"
"source $(Invoke-Sql SaatShop 'SELECT COUNT(*) FROM dbo.Orders;') target $(Invoke-Sql SaatShopReplica 'SELECT COUNT(*) FROM dbo.Orders;')"You should see something like this:
Real order
source 51898 target 51898Now clean up. onprem-sql goes back to its normal state and stays stopped:
aws dms stop-replication-task --replication-task-arn $task | Out-Null
aws dms wait replication-task-stopped --filters "Name=replication-task-arn,Values=$task"
aws dms delete-replication-task --replication-task-arn $task | Out-Null
aws dms wait replication-task-deleted --filters "Name=replication-task-arn,Values=$task"
aws dms delete-replication-instance --replication-instance-arn $ri | Out-Null
aws dms delete-endpoint --endpoint-arn $srcEp | Out-Null
aws dms delete-endpoint --endpoint-arn $tgtEp | Out-Null
aws dms wait replication-instance-deleted --filters "Name=replication-instance-arn,Values=$ri"
aws dms delete-replication-subnet-group --replication-subnet-group-identifier saat-dms-subnets
Invoke-Sql master "ALTER DATABASE SaatShopReplica SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE SaatShopReplica; ALTER SERVER ROLE sysadmin DROP MEMBER dms_user;" | Out-Null
Invoke-Sql SaatShop "EXEC sys.sp_cdc_disable_db; DROP TABLE IF EXISTS dbo.awsdms_truncation_safeguard; ALTER DATABASE SaatShop SET RECOVERY SIMPLE;" | Out-Null
aws ec2 revoke-security-group-ingress --group-id $srcSg --ip-permissions "IpProtocol=tcp,FromPort=1433,ToPort=1433,UserIdGroupPairs=[{GroupId=$dmsSg}]" | Out-Null
aws ec2 delete-security-group --group-id $dmsSg
aws ec2 stop-instances --instance-ids $src | Out-Null
.\Stop-SsmEndpoints.ps1
Remove-Item mappings64.json, task64.json, src-cmd.json✅ Checkpoint: You recovered a task that lost its log position with reload-target, and found and fixed a silent primary key conflict through awsdms_apply_exceptions. The DMS pieces and SaatShopReplica are gone, CDC is off, and onprem-sql is stopping.
This happened at work
The replica that was 3,000 orders short
An e-commerce company used DMS CDC to keep a reporting copy of its orders database. Finance noticed the monthly revenue report was lower than the payment provider’s figures by about 3,000 orders.
The DMS task had been “running” the whole time. A developer had once run a data fix script against the reporting copy by mistake, inserting rows with IDs the source later used. Every conflicting insert since had landed in awsdms_apply_exceptions, which nobody checked.
They reloaded the affected tables, made the replica read-only for everyone except DMS, turned on validation, and added a daily alert when awsdms_apply_exceptions gets new rows.
Common mistakes and troubleshooting
| Mistake | What happens | What to do |
|---|---|---|
| Leaving CDC tasks stopped for days | Log truncated, resume fails | Keep tasks running; alert on stopped/failed |
| Deleting or moving log backups too soon | BACKUP/LOG-s not available | Keep them on the server until DMS is past them |
| Trusting “running” | Silent apply errors | Query awsdms_apply_exceptions; enable validation |
| Writable targets | Key conflicts and drift | Read-only for everyone but DMS until cutover |
| Restarting tasks from scratch for one table | Hours of reload | reload-tables for that table |
| No task logging | Vague failures | EnableLogging and read CloudWatch task logs |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. A DMS task has failed. Where do you look first?
Why they ask this: They want to see that you know where DMS records the reason for a failure, so you can start fixing it quickly.
How you might answer: First I look at the task’s Status, StopReason and LastFailureMessage in the console or CLI. That usually points me to the problem.
Then I read the CloudWatch task log for the details, and check the table statistics to see which tables are affected.
2. Can you explain what the awsdms_apply_exceptions table is for?
Why they ask this: They want to check that you know where DMS hides the changes it couldn’t apply, which is easy to miss.
How you might answer: It’s a control table on the target where DMS writes the changes it couldn’t apply, with the statement and the error. This happens when the error policies are set to log instead of stop.
So the task can look healthy while rows are quietly landing in that table.
3. One table in a DMS task has bad data. What does reload-tables let you do?
Why they ask this: They want to see that you know how to fix one table without restarting the whole migration.
How you might answer: reload-tables reloads just the tables I choose from the source, or only revalidates them. The rest of the task keeps running.
That’s much faster than restarting the whole task for one broken table.
2–5 years experience
4. A CDC task fails on Monday with “BACKUP/LOG-s are not available”. What happened over the weekend?
Why they ask this: They want to see that you understand how DMS depends on the source log and backups, and what breaks when the task is stopped.
How you might answer: The task was stopped, log backups truncated the log, and the backup files DMS needed were deleted or moved. So DMS lost its place and can’t continue.
I’d reload the target to recover. To stop it happening again, I’d change backup retention or keep the task running.
5. Row counts match, but the business says some data is missing. How would you check?
Why they ask this: They want to see that you don’t trust row counts alone, and that you know the deeper ways to compare data.
How you might answer: First I look in awsdms_apply_exceptions for changes that failed. Then I turn on validation, and compare the data by key ranges or checksums.
I also check if anyone has been writing directly to the target, because that can hide or overwrite changes.
6. How would you keep CDC latency low during a long migration?
Why they ask this: They want to see that you know the main levers on both the source and the target side.
How you might answer: I right-size the replication instance, avoid huge transactions on the source, and make sure the target keys are indexed. I use batch apply where it suits the workload, and limit LOB sizes.
Then I watch CDCLatencySource against CDCLatencyTarget to see which side is slowing down.
Senior level
7. You own 30 DMS CDC tasks. How would you design monitoring so nothing fails silently?
Why they ask this: They want to see that you can watch many tasks at once, and catch quiet problems as well as loud failures.
How you might answer: I’d set EventBridge rules on task state changes, and CloudWatch alarms on CDC latency and task errors. Task logs would go to CloudWatch with metric filters.
On a schedule I’d check apply exceptions and validation failures, and the team would have runbooks for deciding when to reload.
8. When would you reload the whole target instead of fixing the problem in place?
Why they ask this: They want to see your judgement on a costly decision, and that you can explain when the slower option is the safer one.
How you might answer: I’d reload the target when the task has lost its log position, many tables are out of sync, or the error history is unclear.
If only one table is affected, reload-tables or a targeted fix is faster.
Quick quiz
Q1. Why did the task fail after resuming?
- Wrong password
- Log it needed was truncated and the backups deleted
- Target full
- Express edition
Show answer
B — The LSN was no longer available.
Q2. What is the fix when the log position is lost?
- Resume again
- Reload the target
- Change the password
- Add memory
Show answer
B — Start from a fresh full load.
Q3. Where did the rejected order show up?
- CloudTrail
- awsdms_apply_exceptions
- S3
- The source error log
Show answer
B — DMS logged the apply error.
Q4. What did the task status show during the PK conflict?
- failed
- running
- stopped
- deleting
Show answer
B — Apply errors aren’t fatal by default.
Q5. Which command reloads just one table?
- start-replication-task
- reload-tables
- modify-endpoint
- test-connection
Show answer
B — With --reload-option data-reload.
Quick recap
- Read Status, LastFailureMessage, table stats and apply exceptions.
- Stopped tasks plus deleted log backups lose the CDC position: reload.
- A running task can still be dropping changes into awsdms_apply_exceptions.
- Keep targets read-only and turn on validation.
- Use reload-tables for one table, reload-target when the position is lost.
Next lesson
Lesson 65: RDS high CPU and slow queries → You’ll find what drives RDS SQL Server CPU with Database Insights, Query Store and DMVs, and fix it.
Further reading: AWS DMS task failed
Want to go deeper into AWS DMS task failed? Read Troubleshooting migration tasks in AWS DMS and Error handling task settings. You can also see the full plan for this course on the AWS Cloud Administration course page.
