Step Functions state machines that never execute cost $0
What does ZopNight detect here?
Step Functions bills $0.025 per 1,000 state transitions and nothing while idle, so a state machine with zero ExecutionsStarted over 30 days already costs $0. ZopNight retired this rule in place, with Evaluate returning nil on every input, rather than ship a $0 delete card under a cost category.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-183 |
| Category | idle |
| Severity | low |
| Metric | none — pure configuration read |
| Source | step_functions_idle.go |
Where it applies
Per-transition pricing leaves nothing to reclaim
A Step Functions state machine is billed on movement: roughly $0.025 per 1,000 state
transitions, metered as executions run. A definition that sits there with zero
ExecutionsStarted triggers zero transitions and lands on the bill at exactly $0. There is
no rack rate to stop paying, no provisioned capacity to shrink, no off-hours window to
schedule, and no commitment to re-tier. Every axis a cost recommendation could quantify
comes back empty. Deleting an already-free state machine saves nothing, so under ZopNight’s
concrete-or-abstain contract for cost findings, the only honest output is no output.
From a $0 advisory card to an honest abstain
The previous implementation knew all of this and emitted anyway: a SavingsUSD 0 finding filed
under advisory, a category the low-savings filter does not treat as cost-driven. That
routing existed purely so the $0 card would survive the filter that exists to drop $0 cards.
The retirement removed the mask: the category is now idle, the honest cost-driven label,
and Evaluate returns nil unconditionally. The rule is retired in place, kept in the registry
for identity stability but incapable of producing a recommendation.
Where ExecutionsStarted still flows
The rule still declares ExecutionsStarted (one of the execution counters Step Functions publishes, dimensioned by the state
machine’s ARN over a 30-day window) as a required metric, so the aggregator keeps harvesting it.
Two plumbing fixes ride along: the resource type key is step-function, matching what the
discoverer actually emits (the old stepfunctions literal matched nothing, leaving the
rule’s type-join dead), and the metric lands on live resources as a result.
Counting executions yourself
aws cloudwatch get-metric-statistics \ --namespace AWS/States --metric-name ExecutionsStarted \ --dimensions Name=StateMachineArn,Value=arn:aws:states:us-east-1:111122223333:stateMachine:my-machine \ --start-time "$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)" \ --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --period 86400 --statistics SumAll-zero sums for a month means nothing started an execution.
Retiring an abandoned state machine
An unused state machine is free but not harmless: its IAM role keeps whatever permissions the workflow once needed, and EventBridge rules or API Gateway integrations may still point at it. If the workflow is genuinely dead, remove the triggers first, then the state machine, then the role, in that order, so nothing fires mid-cleanup.