Sitemap

Serverless Timed Events with DynamoDB TTL and Streams — No Schedulers, No Polling, No Problem

7 min readAug 4, 2025

--

A smart way to delay actions without maintaining timers or cron jobs, using only DynamoDB and a Lambda function.

Press enter or click to view image in full size

What If You Could Delay Actions Without Scheduling Anything?

Let’s be honest — managing scheduled events in the cloud often feels heavier than it should.

Whether you’re using EventBridge Scheduler, Lambda cron expressions, Step Functions with Wait, or even a custom polling mechanism, you're always maintaining something — a timer, a rule, or a heartbeat. That often means you're paying for idle compute, managing retry logic, or just dealing with unnecessary complexity.

But what if you could trigger a delayed action without scheduling anything at all?

What if you could store an item with an expiration time, and simply react to its removal?

Today I want to share a little-known but powerful serverless pattern that combines two AWS services: DynamoDB TTL and DynamoDB Streams. With them, you can delay actions in a fully serverless, event-driven way — no schedulers, no polling, no stress.

Let’s dive into the idea. 🚀

The Idea: TTL + Streams = Magic

Here’s the concept in one sentence:

You write an item to DynamoDB with an expiration time, and when it expires, the item is automatically deleted — that deletion triggers an event via DynamoDB Streams, and that’s where your logic kicks in.

This pattern is surprisingly elegant and extremely lightweight. You don’t need:

  • A cron job checking which items are ready
  • A scheduler to trigger something in the future
  • A Lambda running every X minutes “just in case”

Instead, you rely on DynamoDB TTL (Time to Live) to delete the item when it expires, and you use DynamoDB Streams to observe that deletion. A Lambda function subscribed to the stream will pick up the REMOVE event and handle it however you want — send a notification, clean up a resource, trigger a new workflow, you name it.

💡 Pro tip: This pattern is 100% serverless, completely event-driven, and costs nothing until something actually happens.

It feels like magic — because there’s nothing to manage in between.

Important Caveat: TTL Is Not an Exact Timer

Before you rush to replace all your schedulers with this pattern — there’s an important caveat.

DynamoDB TTL does not guarantee precise expiration timing.

When an item’s TTL (a Unix epoch timestamp) is reached, it becomes eligible for deletion. However, the actual removal from the table does not happen immediately — the process is asynchronous and handled internally by DynamoDB’s background system.

In the past, AWS documentation explicitly mentioned that TTL-based deletions could take up to 48 hours to be processed. While that statement has been removed, and the docs are now more generic, the behavior remains the same: TTL deletions are “eventually consistent.”

In most real-world scenarios, expired items are deleted within a few minutes. But since there’s no SLA on the actual expiration timing, you should treat this mechanism as a soft timer, not a precise one.

Like with many AWS features, it’s all about choosing the right tool for the job.

Use Case Example: Scheduled Push Notifications for Marketing Campaigns

Let’s say your marketing team wants to send push notifications to users at specific times — but without building or maintaining a scheduler, queue system, or a time-based state machine.

With this pattern, you can do exactly that.
Here’s how it works:

  1. A user is enrolled in a campaign — for example, they unlock a promo or reach a milestone.
  2. You write an item to DynamoDB, including metadata like the user ID, message content, and most importantly, an expirationTime attribute set to the exact moment the push should be sent.
  3. DynamoDB TTL kicks in once the expirationTime is reached (give or take a few minutes), and deletes the item.
  4. That deletion event flows through DynamoDB Streams, and triggers a Lambda function.
  5. The Lambda sends the push notification to the user via your chosen provider (SNS, Firebase, Pinpoint, etc).

Here’s a simplified version of what the item might look like:

{
"id": "user-123-campaign-456",
"userId": "user-123",
"message": "🎉 Your 10% off coupon is here!",
"expirationTime": 1722883200, // scheduled push time in Unix epoch
"channel": "push"
}

💡 With this approach, you don’t need to:

  • Poll for “what’s due now”
  • Manage queues or worker containers
  • Keep any infrastructure running in between

It’s a fully serverless, write once, trigger later workflow.

Hands-On: How to Implement It (Step by Step)

Let’s build the use case described above — a simple system to send scheduled push notifications using DynamoDB TTL and Streams.

We’ll use:

  • DynamoDB with TTL enabled
  • DynamoDB Streams to capture REMOVE events
  • AWS Lambda to consume the stream and send the notification

Step 1 — Create the DynamoDB Table

You need a basic table with a primary key (e.g., id), and an attribute to hold the TTL timestamp (e.g., expirationTime).
If you’re using the AWS CLI:

aws dynamodb create-table \
--table-name ScheduledPushes \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--stream-specification StreamEnabled=true,StreamViewType=OLD_IMAGE

Alternatively, you can do this via the AWS Console — just make sure to enable DynamoDB Streams with OLD_IMAGE as the stream view type.

Step 2 — Enable TTL on the Table

You need to tell DynamoDB which attribute represents the TTL timestamp (must be in UNIX epoch format):

aws dynamodb update-time-to-live \
--table-name ScheduledPushes \
--time-to-live-specification "Enabled=true, AttributeName=expirationTime"

You only need to do this once.

Step 3 — Add Items with an Expiration Timestamp

Now, let’s simulate inserting a push notification to be delivered in 5 minutes:

aws dynamodb put-item \
--table-name ScheduledPushes \
--item '{
"id": {"S": "user-123-promo"},
"userId": {"S": "user-123"},
"message": {"S": "🚀 Your limited offer starts now!"},
"channel": {"S": "push"},
"expirationTime": {"N": "1722883200"}
}'

⚠️ Make sure expirationTime is set to a future Unix timestamp in seconds.

Step 4 — Create the Lambda Function

This Lambda will be triggered by DynamoDB Streams and receive a batch of events. Here’s a simple version in Python:

import json

def lambda_handler(event, context):
for record in event['Records']:
if record['eventName'] == 'REMOVE':
old_image = record['dynamodb']['OldImage']
user_id = old_image['userId']['S']
message = old_image['message']['S']

print(f"Sending push to {user_id}: {message}")
# Here you’d call your push service (SNS, Firebase, etc.)

You can expand this with retry logic, monitoring, or sending through a specific channel.

Step 5 — Connect the Stream to the Lambda

In the AWS Console:

  1. Open the Lambda function
  2. Add a trigger
  3. Select DynamoDB
  4. Choose your table and stream
  5. Set batch size and IAM permissions

Or you can wire it all via Infrastructure-as-Code (CDK, SAM, CloudFormation) — but that’s beyond the scope of this walkthrough.

When to Use This Pattern (and When Not To)

This pattern is powerful — but like any tool, it’s not for every scenario.

Think of it as a lazy timer: it works great when you don’t need second-level precision, but still want to trigger delayed actions without constantly checking the clock.

Here’s a quick breakdown:

You should use this when:

  • You want to send scheduled push/email notifications
  • You need to clean up expired sessions, carts, or temporary data
  • You’re building fire-and-forget reminders
  • You want to implement soft retries with backoff delays
  • Your workloads are event-driven and cost-sensitive

You should avoid this if:

  • You need precise execution timing (e.g., at 12:00:00.000)
  • You’re working with security-sensitive tokens or hard deadlines
  • You’re orchestrating multi-step workflows with strong timing dependencies
  • You require a guaranteed delivery time window

If your use case can tolerate the “eventually” part of eventually consistent expiration, this is one of the simplest and most cost-effective ways to trigger future logic in AWS.

🛠️ Bonus Tip: Need More Precision? Add a Controlled Fallback

While DynamoDB TTL is a great no-maintenance solution, its internal expiration timing isn’t guaranteed to be precise.
If you’re dealing with time-sensitive use cases, but still want to keep the simplicity of this pattern, you can schedule a small Lambda function that periodically deletes expired items manually — based on the same expirationTime logic.

This Lambda would:

  • Run every minute (or less)
  • Query for expired items
  • Delete them explicitly
  • Let the Stream trigger the actual logic (just like before)

The benefit? You still centralize the “delayed logic” via item deletion, and keep all your downstream flow exactly the same. The only thing that changes is who initiates the deletion: DynamoDB or you.

This adds minimal cost and complexity — and gives you a bit more control, if needed.

And the best part? If DynamoDB TTL becomes more precise over time, you can simply remove the Lambda — your logic will stay intact and nothing else will need to change.

Conclusion: One of My Favorite Serverless Tricks

I love this pattern because it’s simple, powerful, and surprisingly underused.

In true serverless fashion, it lets you delay actions without running anything in between. Just store an item, let it expire, and react to the deletion — that’s it.

It’s not for everything, and it doesn’t replace schedulers entirely. But for a large class of async use cases — like scheduled notifications, data cleanup, or passive workflows — it’s a super clean solution.

No timers. No polling. No orchestration.

Just DynamoDB, a TTL, a Stream, and a Lambda.

If you’re building event-driven applications on AWS and want to reduce operational overhead, this is a pattern worth adding to your toolbox.

Have you used something like this before? Or are you planning to try it out? Let me know in the comments — I’d love to hear how you’d apply it in your projects.

--

--

Emanuel Russo
Emanuel Russo

Written by Emanuel Russo

Cloud Solution Architect, DevOps Engineer, Senior Software Developer