App Runner Pause/Resume Scheduling
Cutting QA costs by 64% with EventBridge schedules and a Lambda that pauses non-production App Runner services outside business hours.
- DATE:
- APR.11.2026
- READ:
- 8 MIN
The cost problem
App Runner bills per-vCPU-hour for provisioned instances regardless of traffic. A service receiving zero requests at 3 AM costs the same as one handling peak load at noon.
At acmecorp, we run five QA App Runner services. Each provisions 1 vCPU. The math is straightforward: QA engineers work roughly 8 AM to 9 PM IST on weekdays. That leaves 16 hours per weekday and all 48 weekend hours completely idle — 108 of 168 weekly hours burning money for nothing.
That is 64% of compute spend wasted on services nobody touches.
The pause/resume pattern
App Runner supports a native pause operation. A paused service:
- Costs nothing for compute. You pay zero vCPU-hours while paused.
- Returns 503 on its service URL. No ambiguity — clients know the service is down.
- Resumes in 30–60 seconds. Fast enough that a morning schedule beats most engineers to their desks.
- Retains all configuration. Source, environment variables, scaling rules, custom domains — everything persists.
The pattern is simple: pause at end of business, resume at start of business. No infrastructure teardown, no redeployment, no state loss.
The scheduler Lambda
A single Lambda handles both pause and resume. It lists all App Runner services, skips anything that looks like production, and performs the requested action.
SCHEDULER_CODE = """
import boto3
apprunner = boto3.client('apprunner')
PROTECTED_SERVICES = ['prod', 'production', 'demo']
def handler(event, context):
action = event.get('action', 'pause')
services = apprunner.list_services()['ServiceSummaryList']
for svc in services:
name = svc['ServiceName'].lower()
if any(p in name for p in PROTECTED_SERVICES):
continue
arn = svc['ServiceArn']
status = svc['Status']
if action == 'pause' and status == 'RUNNING':
apprunner.pause_service(ServiceArn=arn)
print(f'Paused {name}')
elif action == 'resume' and status == 'PAUSED':
apprunner.resume_service(ServiceArn=arn)
print(f'Resumed {name}')
"""
scheduler_fn = _lambda.Function(
self, "AppRunnerScheduler",
runtime=_lambda.Runtime.PYTHON_3_12,
handler="index.handler",
code=_lambda.Code.from_inline(SCHEDULER_CODE),
timeout=Duration.seconds(120),
)The 120-second timeout gives the Lambda room to iterate through multiple services. Each pause_service and resume_service call is asynchronous on the App Runner side — the Lambda fires the request and moves on.
EventBridge rules
Two EventBridge rules drive the schedule. One resumes services at the start of business, the other pauses them at end of day.
# Resume Mon-Fri at 8:00 AM IST (02:30 UTC)
events.Rule(
self, "ResumeSchedule",
schedule=events.Schedule.cron(
minute="30", hour="2",
week_day="MON-FRI",
),
targets=[targets.LambdaFunction(
scheduler_fn,
event=events.RuleTargetInput.from_object(
{"action": "resume"}
),
)],
)
# Pause Mon-Fri at 9:00 PM IST (15:30 UTC)
events.Rule(
self, "PauseSchedule",
schedule=events.Schedule.cron(
minute="30", hour="15",
week_day="MON-FRI",
),
targets=[targets.LambdaFunction(
scheduler_fn,
event=events.RuleTargetInput.from_object(
{"action": "pause"}
),
)],
)No weekend rules needed. Services paused Friday at 9 PM stay paused until Monday at 8 AM. EventBridge cron uses UTC, so IST offsets are baked into the hour values.
Cost savings
+--------------------+------------+--------------------+---------+ | Scenario | Hours/week | Monthly cost (1 | Savings | | | | vCPU) | | +--------------------+------------+--------------------+---------+ | Always on | 168 | ~$46 | — | +--------------------+------------+--------------------+---------+ | Business hours | 60 | ~$16 | 64% | | only | | | | +--------------------+------------+--------------------+---------+ | Weekdays only | 120 | ~$33 | 28% | | (24h) | | | | +--------------------+------------+--------------------+---------+
For acmecorp’s five QA services, the business-hours schedule saves roughly $150/month — about $1,800/year. The Lambda and EventBridge costs are negligible (well under $1/month).
The savings scale linearly. Ten services doubles it to $300/month. Add staging environments and you are looking at real money returned to the engineering budget.
Alternatives
Before settling on App Runner pause/resume, we evaluated three other patterns:
- ECS Fargate with scheduled scaling to 0. More flexible — you can scale individual tasks, set desired count per schedule, and use step scaling for traffic spikes. But the configuration overhead is higher: task definitions, services, target groups, and scaling policies. Overkill when all you need is on/off.
- Lambda for the QA workloads themselves. Inherently pay-per-use, so no scheduling needed. But QA services at acmecorp are persistent web applications with long-lived connections and startup times measured in seconds. Lambda’s execution model does not fit.
- Fargate Spot. Up to 70% cheaper than on-demand Fargate, but tasks can be interrupted with two minutes notice. QA engineers running manual tests do not appreciate their environment vanishing mid-session.
App Runner’s pause/resume hits the sweet spot: zero-cost downtime with near-instant recovery and zero configuration drift.
Safety net
Schedules fail silently if the Lambda errors out. A CloudWatch alarm catches this.
Set an alarm on the ActiveInstances metric for your App Runner services. During off-hours (after 9 PM IST), the expected count is 0. If the alarm triggers, the pause Lambda likely failed — investigate the Lambda’s CloudWatch logs.
The SSM check is a single addition to the Lambda:
ssm = boto3.client('ssm')
override = ssm.get_parameter(
Name='/acmecorp/scheduler/override'
)['Parameter']['Value']
if override == 'skip':
print('Override active, skipping')
returnThe cheapest infrastructure is the infrastructure that is not running. Pause what you do not need, schedule what you can predict, and alarm on everything else.