<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hasanga Lakdinu</title>
    <description>The latest articles on DEV Community by Hasanga Lakdinu (@hasangalakdinu).</description>
    <link>https://dev.to/hasangalakdinu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F291427%2F5fb9ab34-f3b2-49a3-a1db-618695fe76a4.jpeg</url>
      <title>DEV Community: Hasanga Lakdinu</title>
      <link>https://dev.to/hasangalakdinu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hasangalakdinu"/>
    <language>en</language>
    <item>
      <title>DynamoDB TTL Is Not a Delete Button! It’s the Last Step of a Data Lifecycle</title>
      <dc:creator>Hasanga Lakdinu</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:43:31 +0000</pubDate>
      <link>https://dev.to/hasangalakdinu/dynamodb-ttl-is-not-a-delete-button-its-the-last-step-of-a-data-lifecycle-4e2n</link>
      <guid>https://dev.to/hasangalakdinu/dynamodb-ttl-is-not-a-delete-button-its-the-last-step-of-a-data-lifecycle-4e2n</guid>
      <description>&lt;p&gt;When I first came across DynamoDB TTL, I thought it was pretty simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a timestamp to an item → DynamoDB sees that timestamp → item gets deleted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And technically, that is the basic idea.&lt;/p&gt;

&lt;p&gt;But once you start building a real system, TTL becomes much more interesting.&lt;/p&gt;

&lt;p&gt;The important question isn't just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"When should this item be deleted?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What should happen to this data before it disappears?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, let's look at DynamoDB TTL using a simple AI workflow system as an example.&lt;/p&gt;

&lt;p&gt;We'll see how TTL can work together with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DynamoDB Streams&lt;/li&gt;
&lt;li&gt;S3 archival&lt;/li&gt;
&lt;li&gt;Scheduled jobs&lt;/li&gt;
&lt;li&gt;Single-table design&lt;/li&gt;
&lt;li&gt;Different data lifetimes&lt;/li&gt;
&lt;li&gt;Event-driven applications&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Are We Building?
&lt;/h2&gt;

&lt;p&gt;Imagine we have an AI workflow system.&lt;/p&gt;

&lt;p&gt;A user starts a workflow, and the workflow might execute several steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User starts workflow
       ↓
AI starts processing
       ↓
Step 1
       ↓
Tool call
       ↓
Step 2
       ↓
Final result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the workflow is running, we want to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Show events to the user in real time.&lt;/li&gt;
&lt;li&gt;Allow the user to replay the workflow afterwards.&lt;/li&gt;
&lt;li&gt;Keep a record that the workflow happened.&lt;/li&gt;
&lt;li&gt;Eventually move large historical data to cheaper storage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DynamoDB is a good fit for storing the live workflow data.&lt;/p&gt;

&lt;p&gt;But there is a problem.&lt;/p&gt;

&lt;p&gt;Some of this data is &lt;strong&gt;large and temporary&lt;/strong&gt;, while some of it is &lt;strong&gt;small and important&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's where TTL becomes useful.&lt;/p&gt;




&lt;h1&gt;
  
  
  One DynamoDB Table, Different Types of Data
&lt;/h1&gt;

&lt;p&gt;Let's say our table contains three types of items.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Event items
&lt;/h3&gt;

&lt;p&gt;These represent everything that happened during the workflow.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN_STARTED
STEP_STARTED
TOOL_CALLED
STATE_UPDATED
STEP_COMPLETED
RUN_COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There could be hundreds or thousands of these events for one workflow.&lt;/p&gt;

&lt;p&gt;Some events might even contain large state snapshots.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. META item
&lt;/h3&gt;

&lt;p&gt;We also keep one item containing information about the workflow itself.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"META"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMPLETED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-20T10:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-20T10:05:32Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This item is small.&lt;/p&gt;

&lt;p&gt;And unlike the events, we want to keep it for a very long time.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. User history item
&lt;/h3&gt;

&lt;p&gt;We might also want to answer a question like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Show me my last 20 workflows."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So we keep a small history record associated with the user.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER#123
    RUN#2026-09-20T10:00:00Z
    RUN#2026-09-19T15:30:00Z
    RUN#2026-09-18T08:20:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this information is small and useful long-term.&lt;/p&gt;




&lt;h1&gt;
  
  
  So Where Does TTL Come In?
&lt;/h1&gt;

&lt;p&gt;Here's the important part.&lt;/p&gt;

&lt;p&gt;We &lt;strong&gt;only put the TTL attribute on the event items&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUN#123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EVENT#001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUN_STARTED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1790589600&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But our META item doesn't have &lt;code&gt;expires_at&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUN#123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"META"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMPLETED"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the user history item doesn't have it either.&lt;/p&gt;

&lt;p&gt;This is completely valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  TTL is per item, not per table
&lt;/h2&gt;

&lt;p&gt;This is one of the most useful things to understand about DynamoDB TTL.&lt;/p&gt;

&lt;p&gt;You enable TTL on a table and tell DynamoDB which attribute contains the expiration timestamp.&lt;/p&gt;

&lt;p&gt;But DynamoDB only expires items that actually contain that attribute.&lt;/p&gt;

&lt;p&gt;So one table can contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────┐
│ DynamoDB Table        │
├───────────────────────┤
│ Event                 │ → expires
│ Event                 │ → expires
│ Event                 │ → expires
│ META                  │ → stays
│ User History          │ → stays
└───────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful in single-table designs.&lt;/p&gt;

&lt;p&gt;You don't need a separate table just because two types of data have different lifetimes.&lt;/p&gt;




&lt;h1&gt;
  
  
  But Why Not Delete the Events Immediately?
&lt;/h1&gt;

&lt;p&gt;Because the events still have value.&lt;/p&gt;

&lt;p&gt;Imagine the workflow finishes at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monday 10:00 AM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We might want to keep the events in DynamoDB for a few days because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UI may need to replay them.&lt;/li&gt;
&lt;li&gt;A developer might need to investigate a failed workflow.&lt;/li&gt;
&lt;li&gt;A customer may reopen the workflow.&lt;/li&gt;
&lt;li&gt;Some downstream process might still need them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But keeping large event data in DynamoDB forever isn't necessarily a good idea.&lt;/p&gt;

&lt;p&gt;So we can introduce another step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Archive it.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  The Data Lifecycle
&lt;/h1&gt;

&lt;p&gt;Let's say we want to archive completed workflow events to S3 after 24 hours.&lt;/p&gt;

&lt;p&gt;The lifecycle becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workflow runs
     │
     ▼
DynamoDB
     │
     │ 24 hours
     ▼
Archive to S3
     │
     │ 7 days
     ▼
DynamoDB TTL removes events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T+0
│
├── Workflow events written to DynamoDB
│
│
T+24 hours
│
├── Read events from DynamoDB
├── Compress them
├── Write archive to S3
├── Write manifest.json
└── Mark META as archived
│
│
T+7 days
│
└── DynamoDB TTL eventually removes event items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now notice something important.&lt;/p&gt;

&lt;p&gt;There is a &lt;strong&gt;gap between archival and deletion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That gap is intentional.&lt;/p&gt;




&lt;h1&gt;
  
  
  The TTL Gap Is Your Safety Margin
&lt;/h1&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Archive after 24 hours&lt;/li&gt;
&lt;li&gt;TTL after 7 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You have roughly &lt;strong&gt;6 days of safety margin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why is that important?&lt;/p&gt;

&lt;p&gt;Imagine your archive job fails.&lt;/p&gt;

&lt;p&gt;Maybe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 permissions changed.&lt;/li&gt;
&lt;li&gt;The Lambda deployment introduced a bug.&lt;/li&gt;
&lt;li&gt;One event contains unexpected data.&lt;/li&gt;
&lt;li&gt;The database query fails.&lt;/li&gt;
&lt;li&gt;S3 is temporarily unavailable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your TTL is only 25 hours, you might have almost no time to recover.&lt;/p&gt;

&lt;p&gt;The original data could disappear before you successfully archive it.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24h
│
│ Archive
│
├────────────── 6 days ──────────────┤
│                                    │
│       Safety / recovery window     │
│                                    │
└────────────────────────────────────┘
                                     │
                                     ▼
                                  TTL reap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That six-day period gives your system time to detect and fix failures.&lt;/p&gt;

&lt;p&gt;So TTL isn't just about deciding when to delete something.&lt;/p&gt;

&lt;p&gt;It's also about deciding &lt;strong&gt;how much recovery time you want before deletion becomes possible&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  One Important Thing: TTL Is Not an Exact Timer
&lt;/h1&gt;

&lt;p&gt;This is probably the most important TTL concept.&lt;/p&gt;

&lt;p&gt;If an item has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expires_at = 10:00 AM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that does &lt;strong&gt;not&lt;/strong&gt; mean DynamoDB guarantees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00:00 AM → DELETE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TTL deletion happens asynchronously through a background process.&lt;/p&gt;

&lt;p&gt;So you should think of TTL more like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This item is eligible for deletion after this time."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This item will definitely be deleted at this exact time."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction matters a lot.&lt;/p&gt;




&lt;h1&gt;
  
  
  Don't Use TTL for Time-Critical Security
&lt;/h1&gt;

&lt;p&gt;For example, imagine you have a token that must become invalid exactly at 10:00 AM.&lt;/p&gt;

&lt;p&gt;Don't depend on DynamoDB TTL to enforce that.&lt;/p&gt;

&lt;p&gt;Instead, check the expiration during the read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiresAt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Token is expired&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then TTL can clean the item up later.&lt;/p&gt;

&lt;p&gt;This gives you two separate responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
Enforces expiration

TTL
    ↓
Cleans up old data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a much safer design.&lt;/p&gt;




&lt;h1&gt;
  
  
  Expired Items Can Still Be Read
&lt;/h1&gt;

&lt;p&gt;Another thing that can surprise people:&lt;/p&gt;

&lt;p&gt;An item reaching its TTL doesn't necessarily mean it immediately disappears from queries and scans.&lt;/p&gt;

&lt;p&gt;There can be a delay before DynamoDB removes it.&lt;/p&gt;

&lt;p&gt;So if your application needs strong expiration semantics, don't assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expires_at &amp;lt; now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;item no longer exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, filter it when reading.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query DynamoDB
      ↓
Check expires_at
      ↓
Is it expired?
   /       \
 Yes       No
  ↓         ↓
Ignore     Use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL handles storage cleanup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your application handles business correctness.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  What Happens If DynamoDB Streams Are Enabled?
&lt;/h1&gt;

&lt;p&gt;Now things get even more interesting.&lt;/p&gt;

&lt;p&gt;Suppose we have DynamoDB Streams enabled because we want to push new workflow events to connected clients.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DynamoDB
    │
    ▼
DynamoDB Stream
    │
    ▼
Lambda
    │
    ▼
WebSocket / API
    │
    ▼
Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new event is inserted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;our Lambda can process it.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INSERT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dynamodb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NewImage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TOOL_CALLED"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything looks fine.&lt;/p&gt;

&lt;p&gt;Until TTL starts deleting old events.&lt;/p&gt;




&lt;h1&gt;
  
  
  TTL Deletions Also Appear in DynamoDB Streams
&lt;/h1&gt;

&lt;p&gt;When DynamoDB TTL removes an item, the stream can contain a:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REMOVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;event.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REMOVE"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is where an incorrectly written stream consumer can break.&lt;/p&gt;

&lt;p&gt;Imagine your Lambda does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewImage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable.&lt;/p&gt;

&lt;p&gt;But what happens when the record is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REMOVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;NewImage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So your code can fail.&lt;/p&gt;

&lt;p&gt;And the scary part?&lt;/p&gt;

&lt;p&gt;Your application might work perfectly for weeks.&lt;/p&gt;

&lt;p&gt;Then one day, the first TTL deletions happen.&lt;/p&gt;

&lt;p&gt;Suddenly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda errors
Lambda retries
Dead-letter queue messages
Alerts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;😅&lt;/p&gt;




&lt;h1&gt;
  
  
  Check the Event Type First
&lt;/h1&gt;

&lt;p&gt;A safer approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventName&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"INSERT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewImage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Process the new event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters.&lt;/p&gt;

&lt;p&gt;Don't access &lt;code&gt;NewImage&lt;/code&gt; first and then check whether it's an INSERT.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this an INSERT?
      │
   No │ Yes
      │  │
      ▼  ▼
    Skip  Read NewImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you actually need to process deleted items, then configure your DynamoDB Stream view accordingly.&lt;/p&gt;

&lt;p&gt;For example, if you need the deleted item's previous contents, you can use a stream configuration that provides the old image.&lt;/p&gt;

&lt;p&gt;The important lesson is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Once you introduce TTL, your DynamoDB Stream consumers need to understand that DELETE/REMOVE events can come from the TTL process too.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  A Subtle Design Problem
&lt;/h1&gt;

&lt;p&gt;Here's another interesting problem that can appear in a system like this.&lt;/p&gt;

&lt;p&gt;Imagine we have this permanent history item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER#123
RUN#2026-09-20T10:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp is part of its key.&lt;/p&gt;

&lt;p&gt;Later, the workflow resumes and needs to update that history item.&lt;/p&gt;

&lt;p&gt;But instead of storing the timestamp somewhere permanent, the application gets it by reading the original:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN_STARTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;event.&lt;/p&gt;

&lt;p&gt;And remember...&lt;/p&gt;

&lt;p&gt;That event has a TTL.&lt;/p&gt;

&lt;p&gt;So we have this dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permanent History Item
          │
          │ depends on
          ▼
Temporary RUN_STARTED Event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today this might be perfectly fine.&lt;/p&gt;

&lt;p&gt;The workflow finishes in a few minutes and the event lives for seven days.&lt;/p&gt;

&lt;p&gt;But what happens if someone later changes TTL?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7 days → 1 day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or what if workflows start running for several days?&lt;/p&gt;

&lt;p&gt;Suddenly that assumption becomes dangerous.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Useful Question for Single-Table Designs
&lt;/h1&gt;

&lt;p&gt;Once you introduce different lifetimes into your DynamoDB table, ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"For every item that never expires, does it depend on something that does?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a surprisingly useful design-review question.&lt;/p&gt;

&lt;p&gt;You want to avoid situations like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permanent data
      ↓
Temporary data
      ↓
TTL deletes it
      ↓
Permanent data becomes impossible to update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If something is important enough to live forever, consider whether the information it needs should also be stored permanently.&lt;/p&gt;




&lt;h1&gt;
  
  
  Putting Everything Together
&lt;/h1&gt;

&lt;p&gt;Our complete architecture now looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────┐
                    │   Workflow   │
                    └──────┬───────┘
                           │
                           ▼
                    ┌──────────────┐
                    │   DynamoDB   │
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
           Events         META      User History
              │
              │ TTL
              ▼
        DynamoDB Stream
              │
              ▼
            Lambda
              │
              ▼
       Live UI Updates


After ~24 hours:

Events
   │
   ▼
Archive Job
   │
   ▼
Compress
   │
   ▼
S3
   │
   ▼
manifest.json


After 7 days:

DynamoDB TTL
      │
      ▼
Event items removed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is:&lt;/p&gt;

&lt;h3&gt;
  
  
  DynamoDB
&lt;/h3&gt;

&lt;p&gt;Keeps the data needed for the active/recent workflow experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3
&lt;/h3&gt;

&lt;p&gt;Keeps the historical event data cheaply.&lt;/p&gt;

&lt;h3&gt;
  
  
  META
&lt;/h3&gt;

&lt;p&gt;Keeps the permanent information about the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  User History
&lt;/h3&gt;

&lt;p&gt;Keeps the user's long-term workflow history.&lt;/p&gt;

&lt;h3&gt;
  
  
  TTL
&lt;/h3&gt;

&lt;p&gt;Eventually cleans up the large temporary event data.&lt;/p&gt;




&lt;h1&gt;
  
  
  So, What Is DynamoDB TTL Really?
&lt;/h1&gt;

&lt;p&gt;After looking at a real architecture, I think it's better to think about TTL like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DynamoDB TTL isn't a delete button. It's the final step of a data lifecycle.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before setting a TTL, ask:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Does this data need to be archived?
&lt;/h3&gt;

&lt;p&gt;If yes, where?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DynamoDB → S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. How long does the archive process need?
&lt;/h3&gt;

&lt;p&gt;Maybe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24 hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. How much safety margin do you need?
&lt;/h3&gt;

&lt;p&gt;Maybe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Archive after 24h
TTL after 7 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Does the application need strict expiration?
&lt;/h3&gt;

&lt;p&gt;If yes, enforce it during reads.&lt;/p&gt;

&lt;p&gt;Don't rely only on TTL.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Are DynamoDB Streams enabled?
&lt;/h3&gt;

&lt;p&gt;If yes, make sure consumers handle TTL-generated &lt;code&gt;REMOVE&lt;/code&gt; events.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Does permanent data depend on temporary data?
&lt;/h3&gt;

&lt;p&gt;If yes, think carefully about that dependency.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;TTL looks like a small DynamoDB feature.&lt;/p&gt;

&lt;p&gt;You add an attribute, enable TTL, and DynamoDB eventually removes the item.&lt;/p&gt;

&lt;p&gt;But in a production system, the interesting part isn't the TTL configuration.&lt;/p&gt;

&lt;p&gt;The interesting part is everything &lt;strong&gt;around&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;A good lifecycle might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create
  ↓
Use
  ↓
Archive
  ↓
Keep recovery margin
  ↓
TTL
  ↓
Eventually remove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you start thinking about TTL this way, it becomes much easier to design DynamoDB systems with different data lifetimes.&lt;/p&gt;

&lt;p&gt;And perhaps the biggest lesson is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't ask only "When should DynamoDB delete this?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask "What should happen to this data before DynamoDB is allowed to delete it?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where TTL becomes an architectural decision rather than just a checkbox in the AWS console.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dynamodb</category>
      <category>serverless</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
