Scaling 100 Million Daily Notifications with AWS Step Functions Express: An Architectural Deep‑Dive
Zillow’s Comms Experience team turned a sprawling set of campaign generators into a single, configuration‑driven orchestration platform that fires more than 100 M personalized notifications each day. This article dissects the underlying Step Functions Express workflow, the memory‑model tricks, and the engineering lessons FutureStack teams can steal.
Deep Architectural Breakdown
Step Functions Express as the Execution Engine
Zillow’s notification pipeline is built on AWS Step Functions Express, the low‑latency, high‑throughput variant of the state‑machine service. Unlike Standard Workflows, Express executions are ephemeral (max 5 minutes) and are priced per‑million‑requests plus duration, making them ideal for fire‑and‑forget notification bursts.
Each notification is represented as a short‑lived state machine that stitches together a handful of Lambda functions, a DynamoDB lookup, and a conditional branch that selects the appropriate content module. The state machine definition is stored once in S3 and referenced by a configuration‑driven orchestration layer, so adding a new campaign is a matter of uploading a JSON manifest rather than writing code.
Key properties of the Express runtime that Zillow leverages:
- Micro‑second start‑up: Express runs on a pre‑warmed pool of containers, eliminating the cold‑start latency typical of Standard Workflows.
- Massive concurrency: The service auto‑scales to hundreds of thousands of concurrent executions without user‑managed scaling policies.
- Event‑driven integration: Direct integration with EventBridge, SQS, and SNS lets the pipeline be triggered from any upstream system.
Data Flow and Memory Model
The payload that travels through an execution is deliberately kept under the 4 KB limit for Step Functions input/output. Zillow offloads richer user profiles, recommendation lists, and image assets to Amazon DynamoDB (for key‑value lookups) and Amazon S3 (for large blobs). The workflow follows a pointer‑only model:
- Trigger – An EventBridge rule captures a user‑action (e.g., a new listing view) and writes a minimal event (user‑id, campaign‑id, timestamp) to an SQS queue.
- Start Execution – A Lambda poller reads the SQS message and calls on the Express state machine, passing the tiny envelope.
StartSyncExecution - Enrichment – Inside the workflow, a task pulls the user’s preference profile. The result is stored in the execution context (a JSON document that lives in memory for the duration of the execution).
DynamoDB GetItem - Content Resolution – A state selects a content module (e.g., email, push, SMS) based on the profile. Each module is a reusable Lambda layer that renders a template using the Jinja‑like engine built on AWS Lambda Powertools.
Choice - Dispatch – The final task publishes the rendered message to the appropriate downstream channel (SNS for push, SES for email, Pinpoint for SMS).
Because the execution context is in‑memory only, there is no intermediate persistence cost. The only durable writes are the final audit record to DynamoDB for compliance and a CloudWatch metric for observability.
Latency and Compute Dynamics
Zillow targets sub‑second end‑to‑end latency for most notifications. The breakdown looks like this (averaged over a typical day):
| Stage | Avg Latency | Contributing Factors |
|---|---|---|
| SQS poll → Lambda start | 30 ms | Lambda warm pool, VPC‑less networking |
| Lambda → Step Functions Start | 15 ms | Direct SDK call, Express pre‑warm |
| State Machine orchestration | 40 ms | In‑memory JSON passes, DynamoDB GetItem (single‑digit ms) |
| Content rendering Lambda | 70 ms | Template size < 5 KB, Powertools cache |
| Dispatch (SNS/SES) | 25 ms | Regional endpoint proximity |
| Total | ≈180 ms | — |
The compute dynamics are driven by the fact that each execution runs in a micro‑VM that shares a pool of underlying hardware. The pool is sized automatically by the Step Functions service based on the incoming request rate; Zillow observed a linear scaling curve up to 500 k executions per second before hitting the service‑wide soft limit (which can be raised via a support ticket). The team also tuned the Lambda memory allocation to 256 MiB, balancing CPU share and cost.
Reusable Content Modules
The real engineering win is the library of content modules. Each module is a small, pure‑function Lambda that accepts a JSON payload (
{userId, preferences, campaignData}- Versioned reuse – Multiple campaigns can pin to a specific layer version, enabling safe roll‑backs.
- Zero‑deployment friction – Adding a new campaign is a matter of updating the orchestration manifest, not redeploying code.
- Observability consistency – All modules emit structured logs via the same Powertools logger, making correlation trivial.
The FutureStack Perspective
By Mubashir Arham, Founder & Chief Architect, FutureStack
When I first read Zillow’s post‑mortem on their notification engine, the first thing that struck me was how they turned a chaotic monolith into a declarative, data‑driven pipeline. In many enterprises, the temptation is to keep adding “campaign generators” as separate services, each with its own database, CI/CD pipeline, and monitoring stack. The hidden cost is operational entropy – you end up with 10+ codebases, 10+ alert groups, and a nightmare of cross‑service contracts.
Zillow’s approach solves that by elevating the configuration to the first‑class citizen. The state machine definition lives in a version‑controlled S3 bucket; the only mutable artifact is the JSON manifest that maps a campaign ID to a set of content modules. This yields three practical benefits for FutureStack clients:
- Rapid Experimentation – Data scientists can spin up a new A/B test by dropping a manifest entry; no need to wait for a full CI/CD cycle.
- Predictable Cost Model – Express workflows are priced per‑million‑executions, which aligns perfectly with a notification‑heavy product where you can forecast spend based on MAU.
- Reduced Failure Surface – By consolidating logic into reusable Lambdas, you shrink the number of moving parts. The only thing that can go wrong is a malformed manifest, which is caught by a pre‑deployment schema validator.
Where teams commonly stumble is over‑engineering the orchestration layer. I’ve seen architectures that wrap Step Functions inside another custom scheduler, re‑implementing retry logic that Step Functions already provides. The result is duplicated state, higher latency, and more code to maintain. The lesson here: trust the managed service to do what it does best and keep your custom code minimal.
Another pitfall is payload bloat. Step Functions has a 4 KB limit for each state transition; exceeding it forces you to store data in DynamoDB or S3, which adds latency and cost. Zillow’s pattern of passing only identifiers and pulling the heavy payload on demand is a best‑practice we should all adopt.
Finally, observability is non‑negotiable. By emitting a unified CloudWatch metric for “notifications_sent” and tagging every Lambda with
campaign_idCode / Implementation Blueprint
Below is a Python (boto3) snippet that demonstrates how to launch a Zillow‑style Express workflow from an SQS‑driven Lambda. The code is deliberately minimal – it shows the essential steps: fetch the message, start the execution, and handle throttling back‑off.
import os
import json
import time
import random
import boto3
from botocore.exceptions import ClientError
# Clients are created outside the handler for container reuse
sqs = boto3.client('sqs')
sf = boto3.client('stepfunctions')
QUEUE_URL = os.getenv('NOTIF_QUEUE_URL')
STATE_MACHINE_ARN = os.getenv('EXPRESS_SM_ARN')
def lambda_handler(event, context):
# SQS event can contain multiple records – process them sequentially
for record in event.get('Records', []):
payload = json.loads(record['body'])
# Keep the payload tiny – only identifiers
exec_input = {
"userId": payload["user_id"],
"campaignId": payload["campaign_id"],
"triggerTs": payload["timestamp"]
}
# Exponential back‑off in case Step Functions throttles us
for attempt in range(5):
try:
sf.start_sync_execution(
stateMachineArn=STATE_MACHINE_ARN,
input=json.dumps(exec_input)
)
break # success
except ClientError as e:
if e.response['Error']['Code'] == 'ThrottlingException':
backoff = (2 ** attempt) + random.random()
time.sleep(backoff)
continue
else:
raise
return {"status": "processed", "count": len(event.get('Records', []))}
Explanation of key choices:
- is used for Express because it returns immediately with the execution result, keeping the Lambda short‑lived.
start_sync_execution - The input payload respects the 4 KB limit – only IDs are sent.
- A simple exponential back‑off guards against the rare throttling spikes that can occur during a traffic surge.
Empirical Trade‑Off Table
The following markdown table captures the operational dimensions of Zillow’s Step Functions Express approach versus two common alternatives: a traditional Kinesis‑based fan‑out and a self‑managed microservice orchestrator (e.g., Temporal).
| Dimension | Step Functions Express (Zillow) | Kinesis + Lambda Fan‑out | Temporal (self‑hosted) |
|---|---|---|---|
| Latency (p99) | ~180 ms end‑to‑end | 300‑500 ms (Kinesis propagation) | 150‑250 ms (depends on worker pool) |
| Cost (per M executions) | $0.40 (SF) + $0.20 (Lambdas) | $0.60 (Kinesis) + $0.30 (Lambdas) | $0.00 (software) + EC2/FS costs (~$2‑3) |
| Scalability | Automatic to 500 k exec/s (soft limit) | Limited by shard count (manual) | Unlimited with proper fleet sizing |
| Operational Complexity | Low – fully managed, schema‑driven | Medium – shard management, ordering guarantees | High – cluster ops, DB backup, versioning |
| Observability | Built‑in CloudWatch metrics, execution history | CloudWatch + custom metrics | OpenTelemetry + custom dashboards |
| Retry / Compensation | Native state‑machine retries, Catch blocks | Manual DLQ handling | Built‑in workflow compensation |
| State Size Limit | 4 KB per transition (pointer model) | 1 MB per record (larger payload) | Unlimited (DB‑backed) |
When to Choose Each
- Step Functions Express shines when you need massive concurrency with sub‑second latency and you can keep the payload small.
- Kinesis fan‑out is appropriate when you require ordered streams or need to retain events for replay.
- Temporal is the go‑to for complex saga‑style workflows where you need long‑running state, versioned workflow definitions, and full control over retries.
Future Industry Trajectory (18‑36 Months)
The notification problem space is evolving along three converging trends:
- Event‑Driven Serverless Orchestration Becomes the Default – As AWS, Azure, and GCP continue to lower the latency of their managed state‑machine services (e.g., Azure Durable Functions Premium, GCP Workflows), the express tier will become the de‑facto pattern for any high‑throughput fan‑out use case, not just notifications.
- AI‑Generated Content Modules – With generative LLMs being offered as managed endpoints, future content modules will embed on‑the‑fly copy generation. The orchestration layer will merely pass a prompt and a user context to a Lambda that calls an LLM, dramatically reducing the need for pre‑written templates.
- Observability‑Driven Autoscaling – Emerging services like AWS Application Composer and Step Functions Distributed Tracing will let teams define scaling policies based on business‑level SLAs (e.g., “95 % of push notifications must be delivered within 200 ms”). This will push the industry toward SLA‑as‑code where the orchestration definition includes latency budgets.
For FutureStack, the strategic implication is clear: invest in tooling that abstracts the manifest‑driven orchestration pattern and integrates AI‑assisted content generation. By providing a developer portal that validates manifests against a schema, auto‑generates Lambda Layer scaffolding, and surfaces real‑time SLA metrics, we can enable our customers to replicate Zillow’s success without reinventing the wheel.
Author’s note: All AWS service names and limits are accurate as of November 2026. Prices are quoted in US $ and reflect the standard on‑demand rates for the us‑east‑1 region.
Responsive Ad Placement (728×90 / Dynamic). Live ads will serve once your publisher ID is linked.

Mubashir Arham
CEO & Founder of futurestack.online | Full Stack Developer | BS Artificial Intelligence
CEO, Developer of futurestack.online, Digital Entrepreneur, Full Stack Developer, Author, Digital Mentor, Owner, and Bachelor of Artificial Intelligence (BS AI). Architecting the frontier of autonomous AI software systems, scalable cloud stacks, and digital knowledge products.
Read all publications by Mubashir Arham
