For services handling high-stakes operations such as financial transactions, order processing, or eligibility decisions, regression testing has to catch behavior changes before production deployment. Traditional integration tests help, but they can suffer from unreliable dependencies that cause false positives and slow down deployment pipelines. In this post, I walk through how I built an automated regression testing framework for my team using AWS services. Each run of the framework is a simulation: it replays a sample of production traffic against the new code and compares the results to validate that changes do not break existing functionality.

To build this framework, you need a service whose requests and responses you can capture to a stream such as Amazon DynamoDB Streams, a pre-production environment that can receive replayed requests, and a deployment pipeline with a gate that can read an Amazon CloudWatch alarm.

Minimally invasive design

The framework’s power lies in its modular design. Components fit between existing blocks in your system with minimal changes to your core business logic. Stateless systems need no changes. Stateful systems add a check that recognizes replayed requests and skips side effects. The same components capture, replay, and validate traffic at the API gateway, between microservices, or at internal service boundaries.

Focus: Regression detection

This framework specifically targets no-impact validation, confirming that code changes do not break existing behavior. It does not test new business logic. A/B testing and user acceptance testing handle that. Instead, the framework gives engineering teams confidence that refactors, upgrades, and feature-flagged deployments maintain existing functionality.

Scope and applicability

The framework works best with computation-heavy, stateless systems, where replaying a request produces the same output without side effects. Examples include pricing calculators, validation engines, eligibility decision engines, and data transformation pipelines.

Stateful systems need more coordination. These include transaction processing (payments, orders, inventory) and non-idempotent writes that would create duplicate records or notifications. They also include randomized logic such as A/B assignment, and multi-step workflows such as shopping carts, where each request depends on earlier state. “Handling stateful dependencies” later in this post covers patterns for these systems.

Proven results

In more than 6 months of production use in our system, the framework caught five regressions that would have affected customers, with zero false positives that blocked valid deployments. Teams can set alarms on the share of replayed requests whose results match production (commonly 99.9 percent) to block potentially problematic deployments. This maintains faster deployment velocity through automated quality gates. The framework also standardizes testing approaches across all AWS Regions, which gives consistent validation regardless of deployment location.

Use cases: When and why to simulate for regression detection

The framework addresses three critical scenarios where engineering teams need confidence that code changes do not break existing functionality:

1. Refactoring and technical upgrades

Teams refactoring internal logic or upgrading dependencies need confidence that behavior hasn’t changed. Post-deployment monitoring only catches issues after they affect customers. Simulation provides pre-deployment validation with zero customer impact.

Example: A team upgraded their data processing algorithm’s internal data structures for performance. Unit tests passed, and simulation verified that complex real-world request patterns still produced identical business logic outputs across all Regions.

2. Feature toggle deployments

Code deployed behind feature flags creates a unique challenge: the code is deployed but not yet active. Engineering teams need confidence during the deployment phase, whereas product teams monitor the feature after toggle activation using standard product metrics.

Example: A team added a new validation rule behind a feature flag. Simulation ran against the new code path using production traffic, validating that the deployment itself didn’t introduce regressions before the product team turned on the feature.

3. Validating impact boundaries for significant changes

When product teams predict a change will affect a specific percentage of requests, engineering teams need confidence in that boundary. The change should not affect other request types.

Example: Product predicted a business rule change would affect 15 percent of requests in specific Regions. Simulation confirmed 14.2 percent impact within the predicted request patterns and 0 percent impact on other request types, validating that the change didn’t break functionality outside its intended scope.

Architecture overview: Versatile component-based design

Figure 1 shows the framework. When a deployment reaches the deployment gate, the pipeline publishes an event to Amazon Simple Notification Service (Amazon SNS). Amazon SNS delivers it to an Amazon Simple Queue Service (Amazon SQS) queue. An AWS Lambda function reads the queue and starts the AWS Step Functions workflow. The workflow turns replay on and off. While replay is on, the replay function reads captured requests from the Amazon DynamoDB stream and sends them to the new code. It logs a match result for each request to Amazon CloudWatch Logs. A CloudWatch alarm on the match fraction tells the deployment gate whether to promote the deployment.

Architecture diagram of the regression testing framework

Figure 1: Regression testing framework architecture

Component insertion flexibility

The framework is designed to be versatile. Each component can be positioned to capture inputs and outputs at any system boundary:

  • API level: Capture HTTP requests and responses at gateway or service endpoints.
  • Service level: Capture inter-service communication and message passing.
  • Database level: Capture query inputs and results for data layer validation.
  • Message queue level: Capture message processing for event-driven architectures.

You can compare end-to-end outputs or scope the comparison to one block of business logic. Anything after that block, such as formatting or downstream API calls, falls outside the comparison.

Tech stack and rationale

I chose managed AWS services to keep operational overhead low and to pay only for the resources each simulation consumes:

  • AWS Step Functions: Visual workflow orchestration with built-in error handling and state management for coordinating timed operations.
  • AWS Lambda: Serverless execution with automatic scaling. Pay only for actual simulation time.
  • Amazon DynamoDB: Low-latency access to production request data with global tables for multi-Region support. (Note: We used DynamoDB as part of our existing tech stack. You can adapt the framework to the storage system you currently use.)
  • Amazon CloudWatch: Unified logging, metrics, and alarming. Amazon CloudWatch Logs Insights supports complex queries without additional infrastructure.
  • Amazon SNS and Amazon SQS: Reliable publish/subscribe messaging for pipeline integration with retry logic and dead-letter queues.

Component 1: Traffic capture – universal storage with complete parameter logging

Core function: Captures actual production request documents and their associated responses, not synthetic data or mocks. This complete production dataset is what makes regression detection accurate by replaying the same requests that flowed through the production system with all their complexity and edge cases.

Key challenge: Capturing all parameters and context needed to reproduce the component’s behavior accurately. Missing any input parameter, request context, or metadata can lead to inaccurate replay and false regression signals.

Apply domain-specific knowledge to identify and capture all inputs your system needs for accurate replay. This includes obvious parameters (request body, headers) and less obvious context (user session state, feature flags, regional settings, timestamps). The completeness of your capture directly determines replay accuracy.

Storage flexibility: The framework design works with any storage system that can generate events:

  • Amazon DynamoDB Streams: For document-based systems.
  • Amazon Relational Database Service (Amazon RDS) and Amazon Aurora: Using database triggers or change data capture.
  • Amazon Kinesis Data Streams: For high-throughput event streaming.
  • CloudWatch Logs: For API gateway or application-level capture.
  • Amazon Managed Streaming for Apache Kafka (Amazon MSK): For Kafka-based architectures.

Component 2: Replay engine – running simulated production traffic against test environments

Core function: Takes batched production requests from storage streams and invokes test environment APIs with the same parameters, capturing responses for comparison.

Key challenge: The system needs to distinguish replayed traffic from production traffic. Without this distinction, replayed requests would be processed identically to production requests, leading to duplicate processing and unintended side effects.

The replay engine solves this by attaching simulation IDs to every replayed request:

// Add simulation markers to every replayed request request.addProperty("simulationId", simulationId); request.addProperty("isSimulation", true);

Component 3: Comparison engine – domain-specific output analysis

Core function: Compares specific business logic outputs between production and simulation responses to calculate match fractions. The engine extracts the outputs you’ve chosen to focus on and determines whether they match within acceptable thresholds.

Key challenge: Determining what to compare for your specific application. This requires deep domain knowledge. You need to identify which outputs represent your core business logic as opposed to downstream formatting, third-party variations, or environmental differences that do not indicate actual regressions.

Before logging, reduce each response to the fields that carry your business logic, and normalize them so equivalent outputs compare equal, for example by dropping timestamps and sorting lists. The replay engine compares the reduced results and logs one JSON line for each request. Each line has a numeric match field that is 1 when the results are equal and 0 when they differ or the replay fails. Both results stay in the line for investigating mismatches:

{"SimulationId": "<simulationId>", "match": 1, "prod": "<reduced production result>", "sim": "<reduced simulation result>"}

CloudWatch Logs Insights discovers fields in JSON log events, and the query averages match for one simulation:

filter SimulationId = "<simulationId>" | stats avg(match) as matchFraction

When the replay completes, run the query and publish the match fraction as a custom CloudWatch metric for the deployment gate’s alarm. The implementation reference at the end of this post shows a Lambda function that does this. What constitutes a meaningful comparison depends on your system architecture and business requirements. For the full query language, see the CloudWatch Logs Insights query syntax.

Component 4: Orchestration – CI/CD pipeline integration

Core function: Manages the timing and coordination of simulation components: starts traffic capture, waits for sufficient data collection, stops capture, evaluates results, and provides pass or fail decisions for deployment gates.

Key challenge: Integrating simulation results into continuous integration and continuous delivery (CI/CD) pipelines as reliable quality gates. Deployments need automated pass or fail decisions based on simulation outcomes, not only monitoring data that requires manual interpretation.

Deployment events reach the AWS Step Functions workflow through Amazon SNS, Amazon SQS, and a Lambda function that starts the execution, with retries and error handling.

Replay starts and stops by enabling and disabling the event source mapping between the capture stream and the replay function. Lambda can take several minutes to stop or restart polling after the mapping changes. The workflow waits a few minutes after stopping replay before it evaluates results. When replay is enabled again, the mapping can resume with records captured while it was off. Set a maximum record age on the mapping to skip stale records, and use reserved concurrency on the replay function to cap the burst.

A CloudWatch alarm on the match fraction blocks the deployment when it falls below your threshold. In AWS CodePipeline, for example, an entry condition with a CloudWatchAlarm rule on the production stage blocks promotion while the alarm is in the ALARM state. Otherwise, the pipeline promotes the deployment. The implementation reference at the end of this post shows the state machine definition and the start and stop functions.

Simulation traffic controls and production safety

Critical safeguards prevent overwhelming test environments and maintain production safety:

// Hard limits prevent accidental overload if (props.trafficFraction > 0.1) throw new Error('Traffic fraction too high'); // Concurrency controls limit resource usage reservedConcurrentExecutions: 3

Typical configuration: 5 percent traffic replay with 200-record batching provides sufficient coverage while maintaining system stability.

Handling stateful dependencies

As noted in “Scope and applicability,” systems with stateful components can use this framework with additional patterns.

1. Idempotency controls

Services in the simulated logic path can use the simulation IDs described in Component 2 to detect replayed requests. They then skip stateful operations (database writes, notifications, external API calls) while still returning a response.

// Service checks for replay if (!request.isSimulation()) { database.write(result); notificationService.send(result); }

For services that need further isolation, simulation IDs can also be used to route simulation traffic to separate test instances or maintain separate simulation state.

2. Deterministic randomization

For systems using randomization (A/B assignment, sampling, shuffling), use the simulation ID as a seed:

// Production: true randomization Random rand = new Random(); // Simulation: deterministic based on request if (request.isSimulation()) { rand = new Random(request.getSimulationId().hashCode()); }

This makes simulation responses reproducible and comparable.

When to reconsider

If adding idempotency infrastructure creates an ongoing maintenance burden that exceeds the value of automated regression testing, consider whether this framework is the right fit. Manual testing or alternative validation approaches might be more practical for your system.

Cost and operational efficiency

The framework’s own Lambda functions, logging, streaming, and orchestration cost little. The primary cost driver is your own system processing replayed requests, which varies based on your architecture. The following is a breakdown of the framework costs only. Prices shown are for the US East (N. Virginia) AWS Region as of October 2026. To estimate your own costs, use the AWS Pricing Calculator.

Cost components:

  • Lambda executions: $0.0000166667 per GB-second plus $0.0000002 per request.
  • CloudWatch Logs: $0.50 per GB ingested plus $0.03 per GB stored (monthly).
  • CloudWatch Logs Insights: $0.005 per GB scanned per query.
  • Step Functions: $0.025 per 1,000 state transitions.
  • DynamoDB Streams: no read charge when standard AWS Lambda functions read the stream (Lambda Managed Instances incur Streams charges).
  • Amazon SNS and Amazon SQS: $0.40–$0.50 per million requests.

Example calculation (24-hour simulation, 5 percent of 100,000 daily requests):

  • 5,000 replayed requests × 1 second average duration × 256 MB = about $0.02 Lambda cost.
  • 5,000 log entries × 5 KB average = 25 MB logs = about $0.013 ingestion plus about $0.001 storage.
  • DynamoDB Streams reads: $0 (included in the preceding Lambda cost).
  • One evaluation query scanning 25 MB = about $0.0001 Logs Insights.
  • Step Functions and messaging costs = about $0.01.

Total estimated cost: about $0.05 for this example scenario.

Multi-Region deployment multiplies costs by the number of Regions. Higher traffic volumes scale proportionally. For example, 500,000 daily requests (25,000 replayed) cost approximately $0.25 per simulation.

Scale the calculation based on your traffic volume and simulation frequency. Most importantly, the confidence to deploy faster and the hours saved on manual validation far exceed the simulation costs.

Getting started: Phased implementation approach

Build the framework in three phases. Each phase is usable before the next one starts.

Phase 1 – Core components: Set up traffic capture with your chosen event source, create a replay engine with simulation IDs, implement centralized output analysis with structured logging, and build orchestration with Step Functions.

Phase 2 – Automated verification: Design a match fraction calculation with business logic for filtering variances, create CloudWatch Logs Insights queries, and set up CloudWatch alarms for quality gates.

Phase 3 – Full automation: Integrate with deployment pipelines, deploy across multiple Regions, build monitoring dashboards, and tune thresholds based on real data.

Implementation reference

The following sample code is generalized from my team’s production implementation and is not intended for production use as written. Replace the placeholder names and Amazon Resource Names (ARNs) with your own.

Replay function

@Log4j2 public class ReplayHandler implements RequestHandler<DynamodbEvent, Void> { private static final ObjectMapper JSON = new ObjectMapper(); private final Random random = new Random(); private final SsmClient ssm = SsmClient.create(); private final TestServiceClient testService = new TestServiceClient(System.getenv("TEST_ENDPOINT")); @Override public Void handleRequest(DynamodbEvent event, Context context) { SimulationProps sim = SimulationProps.fromJson( ssm.getParameter(r -> r.name(System.getenv("SIMULATION_PARAMETER"))).parameter().value()); for (DynamodbEvent.DynamodbStreamRecord record : event.getRecords()) { if (!"INSERT".equals(record.getEventName())) { continue; } CapturedCall captured = CapturedCall.from(record.getDynamodb().getNewImage()); if (captured.request().isSimulation() || random.nextDouble() > sim.trafficFraction()) { continue; } String prodResult = reduce(captured.response()); String simResult; try { simResult = reduce(testService.call(captured.request().withSimulationId(sim.id()))); } catch (Exception e) { simResult = "REPLAY_FAILED: " + e.getMessage(); } log.info(JSON.createObjectNode() .put("SimulationId", sim.id()) .put("match", prodResult.equals(simResult) ? 1 : 0) .put("prod", prodResult) .put("sim", simResult) .toString()); } return null; } }

The AWS Systems Manager Parameter Store parameter named in SIMULATION_PARAMETER holds:

{"id": "<execution name>", "trafficFraction": <fraction of traffic to replay>}

CapturedCall, SimulationProps, TestServiceClient, and reduce stand in for your own request model, simulation settings, service client, and response reduction. reduce returns the normalized business-logic fields as a string.

State machine definition

{ "StartAt": "StartSimulation", "States": { "StartSimulation": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "<start-simulation-function-arn>", "Payload": {"id.$": "$$.Execution.Name", "trafficFraction.$": "$.trafficFraction", "durationSeconds.$": "$.durationSeconds"} }, "OutputPath": "$.Payload", "Catch": [{"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "StopSimulation"}], "Next": "WaitForSimulation" }, "WaitForSimulation": { "Type": "Wait", "SecondsPath": "$.durationSeconds", "Next": "StopSimulation" }, "StopSimulation": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": {"FunctionName": "<stop-simulation-function-arn>", "Payload.$": "$"}, "OutputPath": "$.Payload", "Next": "WaitForInFlightReplays" }, "WaitForInFlightReplays": { "Type": "Wait", "Seconds": 300, "Next": "EvaluateSimulationResults" }, "EvaluateSimulationResults": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "<evaluate-simulation-results-function-arn>", "Payload": {"id.$": "$$.Execution.Name", "simulationStartTime.$": "$$.Execution.StartTime"} }, "End": true } } }

Start and stop functions

import {LambdaClient, UpdateEventSourceMappingCommand} from "@aws-sdk/client-lambda" import {PutParameterCommand, SSMClient} from "@aws-sdk/client-ssm" const lambda = new LambdaClient({}) const ssm = new SSMClient({}) const setReplayEnabled = (enabled: boolean) => lambda.send(new UpdateEventSourceMappingCommand({ UUID: process.env.REPLAY_LAMBDA_TRIGGER_UUID, Enabled: enabled, })) export const startSimulation = async (event: {id: string; trafficFraction: number; durationSeconds: number}) => { await ssm.send(new PutParameterCommand({ Name: process.env.SIMULATION_PARAMETER, Value: JSON.stringify({id: event.id, trafficFraction: event.trafficFraction}), Type: "String", Overwrite: true, })) await setReplayEnabled(true) return event } export const stopSimulation = async (event: object) => { await setReplayEnabled(false) return event }

Evaluation function

import {CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand} from "@aws-sdk/client-cloudwatch-logs" import {CloudWatchClient, PutMetricDataCommand} from "@aws-sdk/client-cloudwatch" const logs = new CloudWatchLogsClient({}) const cloudwatch = new CloudWatchClient({}) const matchFractionQuery = (simulationId: string) => `filter SimulationId = "${simulationId}" | stats avg(match) as matchFraction` const publishMatchFraction = (value: number) => cloudwatch.send(new PutMetricDataCommand({ Namespace: "RegressionSimulation", MetricData: [{MetricName: "MatchFraction", Value: value, Unit: "None"}], })) export const handler = async (event: {id: string; simulationStartTime: string}) => { const {queryId} = await logs.send(new StartQueryCommand({ logGroupName: process.env.REPLAY_LOG_GROUP, startTime: Math.floor(Date.parse(event.simulationStartTime) / 1000), endTime: Math.floor(Date.now() / 1000), queryString: matchFractionQuery(event.id), })) let result do { await new Promise((resolve) => setTimeout(resolve, 5000)) result = await logs.send(new GetQueryResultsCommand({queryId})) } while (result.status === "Scheduled" || result.status === "Running") const value = result.results?.[0]?.find((field) => field.field === "matchFraction")?.value if (result.status !== "Complete" || !value) { await publishMatchFraction(0) throw new Error(`Simulation ${event.id} produced no match fraction (query status ${result.status})`) } const matchFraction = parseFloat(value) await publishMatchFraction(matchFraction) return {simulationId: event.id, matchFraction} }

Conclusion

This component-based simulation framework transformed our deployment process from manual and time-intensive to automated and reliable, saving hours per deployment while achieving high accuracy in regression detection. The modular design means the same building blocks can be inserted anywhere in your system architecture to validate that code changes do not break existing functionality.

Look at how your team catches regressions today. If refactors, dependency upgrades, or feature-flagged releases reach production with only synthetic test coverage, or if regressions surface first in post-deployment alarms, replaying production traffic before deployment closes that gap. Start with one stateless component, such as a pricing or validation engine, where replay needs no changes to your service code.

To get started, see the AWS Step Functions detail page and the AWS Step Functions Developer Guide. This framework also uses AWS Lambda, Amazon DynamoDB, Amazon CloudWatch, Amazon SNS, and Amazon SQS.


About the author

Hannah Zhang

Hannah Zhang

Hannah is a Software Development Engineer on the Worldwide Returns and Recommerce Core Tech team at Amazon. Hannah builds large-scale, low-latency services that process high volumes of production traffic.