Streams and TTL

Amazon DynamoDB · Advanced · 14 min

Event-driven side effects and automatic cleanup are core building blocks of serverless DynamoDB applications.

DynamoDB Streams records every item-level change (insert, modify, remove) in order per item, retained for 24 hours. The StreamViewType decides what each record carries: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE or NEW_AND_OLD_IMAGES.

The usual consumer is AWS Lambda through an event source mapping: Lambda polls the stream and invokes your function with batches of records. Use it to update aggregates, keep a search index in sync, send notifications, or replicate to another store. Records for the same item arrive in order; a function that throws will be retried with the same batch, so handlers must be idempotent, and you should configure a failure destination and BisectBatchOnFunctionError so one bad record cannot block a shard. Alternatively stream changes to Kinesis Data Streams for longer retention and more consumers.

Time to Live (TTL): choose an attribute holding an expiry time as a Unix epoch number in seconds. After that time DynamoDB deletes the item in the background at no write cost, typically within a few days, not instantly. Filter out expired-but-present items in queries (expires_at > :now) when it matters. TTL deletions appear in the stream with a userIdentity of dynamodb.amazonaws.com, so you can archive expired items to S3.

Example

# Lambda handler attached to the Orders table stream (NEW_AND_OLD_IMAGES)
import time

def handler(event, context):
    for rec in event["Records"]:
        name = rec["eventName"]                    # INSERT | MODIFY | REMOVE
        keys = rec["dynamodb"]["Keys"]             # wire format: {"customer_id": {"S": "c1"}, ...}
        if name == "MODIFY":
            old = rec["dynamodb"]["OldImage"]["status"]["S"]
            new = rec["dynamodb"]["NewImage"]["status"]["S"]
            if old != new == "SHIPPED":
                print("notify", keys["customer_id"]["S"])   # must be idempotent
        elif name == "REMOVE" and rec.get("userIdentity", {}).get(
                "principalId") == "dynamodb.amazonaws.com":
            print("expired by TTL", keys)

# writing an item that expires in 7 days (TTL attribute "expires_at")
item = {"customer_id": "c1", "order_id": "cart",
        "expires_at": int(time.time()) + 7 * 24 * 3600}

Remember

  • Streams keep 24 hours of ordered item changes; Lambda is the usual consumer
  • Stream handlers are retried, so make them idempotent
  • TTL takes an epoch-seconds number and deletes expired items for free
  • TTL deletion is delayed; filter expired items if exactness matters

Watch out for

  • Storing the TTL attribute as an ISO date string or milliseconds; TTL then never deletes the item.

Check yourself

  1. An item's TTL attribute passed an hour ago. What can a query still return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html