AWS Step Functions caps the data passed into and out of a state. What is that limit, and how do you design a workflow whose steps produce large results?
answer
- the cap is on the payload, not the work
- kilobytes, not megabytes
- pass the address, not the parcel
- merging results makes payloads grow
- trim the integration's wrapper early
basics
~20 sState input and output are capped at 256 KiB as of 2025, and exceeding it fails the execution with a data-limit error. Keep large data in S3 and pass object keys through the workflow, trimming each state's output so payloads never accumulate.
solid answer
~50 sAs of 2025 the input or output of a state — and of the execution itself — is limited to 256 KiB of UTF-8 data; crossing it fails the execution with `States.DataLimitExceeded`. Two design moves keep you under it. First, **never carry the data, carry a pointer**: a Task writes its result to S3 and returns a bucket and key, and the next Task reads it. Second, **trim aggressively at each hop**: by default a state's whole result becomes the next state's input, and `ResultPath` merges results into the payload so it grows monotonically down a long workflow. Use `ResultSelector` to keep only the fields you need, `OutputPath` to discard the rest, and — on newer state machines — variables assigned with `Assign` to carry small values without threading them through the payload at all. In a Distributed Map, `ResultWriter` does the same job for per-item results.
code
json · 28 lines{
"Comment": "Carry an S3 pointer, not the data",
"StartAt": "Extract",
"States": {
"Extract": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "extract", "Payload.$": "$" },
"ResultSelector": {
"bucket.$": "$.Payload.bucket",
"key.$": "$.Payload.key",
"rowCount.$": "$.Payload.rowCount"
},
"ResultPath": "$.extract",
"Next": "Transform"
},
"Transform": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "transform",
"Payload": { "bucket.$": "$.extract.bucket", "key.$": "$.extract.key" }
},
"OutputPath": "$.Payload",
"End": true
}
}
}go deeper
Know that data moving between states is JSON and that there is a size cap in the hundreds of kilobytes, so a Task should return a summary or a reference rather than a file's contents.
Explain how each path field shapes the payload, especially that ResultPath merges while omitting it replaces, and show the S3-pointer pattern in a concrete two-state example.
Diagnose the late failure: an expensive step succeeds, the fan-in blows the cap, and the run is lost. Talk about trimming at each hop and about intermediate S3 artifacts as a debugging asset.
Own the convention across teams — payloads carry identity and metadata only, bytes live in object storage with a retention policy — so no workflow rediscovers this limit in production.
## The limit As of 2025, Step Functions caps the JSON payload for a state's input or output — and for an execution's input or output — at **256 KiB**. It is not a soft throttle: when a state produces more than that, the execution fails with a data-limit error (`States.DataLimitExceeded`), and it usually fails *late*, after the expensive step has already run. This catches people out because the limit is on the *payload*, not on the work. A Lambda that reads a 2 GB file and returns a one-line summary is fine. A Lambda that returns the parsed contents of a 1 MB CSV is not. ## The pointer discipline The fix is old and boring: keep the data where data lives, and pass a reference. ```json "Extract": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Parameters": { "FunctionName": "extract", "Payload.$": "$" }, "ResultSelector": { "bucket.$": "$.Payload.bucket", "key.$": "$.Payload.key", "rowCount.$": "$.Payload.rowCount" }, "ResultPath": "$.extract", "Next": "Transform" } ``` The function writes its output to S3 and returns three small fields. The workflow carries identity and metadata; S3 carries bytes. As a bonus you get a durable artifact of each stage that you can inspect after an incident, which the payload would never have given you. ## Why payloads grow when you are not looking The path fields determine what survives each hop, and their defaults are generous: - **`InputPath`** selects a portion of the incoming payload before the state sees it (default: all of it). - **`Parameters`** builds the exact object passed to the integration. - **`ResultSelector`** reshapes the integration's raw result — this is where you drop the wrapper that optimized integrations return. A `lambda:invoke` result, for instance, nests your value under `Payload` alongside metadata; keeping the whole envelope for twenty states is pure waste. - **`ResultPath`** decides where the result is grafted onto the input. Omit it and the result *replaces* the payload; set it to a field and the payload *accumulates*. Accumulation is what silently grows a workflow's payload until step nineteen blows the limit. - **`OutputPath`** takes the final cut of what moves on. So the discipline is: select narrowly with `ResultSelector`, and be deliberate about whether `ResultPath` is merging or replacing. ## Variables, on newer state machines Since late 2024, Step Functions supports **variables**: a state can `Assign` a value to a named variable, and later states read it directly instead of it being threaded through every intermediate payload. The same release added JSONata as an alternative query language to JSONPath, selected with the top-level `QueryLanguage` field. Variables do not raise the 256 KiB limit, but they remove one of the main *reasons* payloads bloat — carrying a value through ten states that do not use it just so state eleven can see it. ## The places the limit bites hardest - **Fan-in.** A `Parallel` state returns an array of branch outputs; a `Map` returns an array with one entry per item. Both aggregate, and both can exceed the cap even when each individual result is small. Trim each branch's output, or write results to S3 and aggregate keys. - **Distributed Map.** `ResultWriter` exists precisely so per-item results go to S3 instead of into the state's output. - **API responses.** SDK integrations return whatever the API returns, and some list operations return a great deal. `ResultSelector` on the spot. - **Execution input.** The 256 KiB cap also applies to what you pass to `StartExecution`, so a large upstream event must itself become a pointer. ## How to answer this in an interview State the number with its "as of", then move immediately to design: the interviewer is testing whether you reach for the pointer pattern and payload hygiene, or whether you propose splitting the workflow to dodge a limit you did not need to hit. Mention that keeping bytes in S3 also gives you inspectable intermediate artifacts — that is the answer of someone who has debugged a failed run at three in the morning.
- Why does a long workflow's payload often grow even when every step returns something small?Because `ResultPath` merges a state's result into the existing payload instead of replacing it. Twenty states each grafting a small object onto the same document produce a large document. Either omit `ResultPath` where the result should replace the payload, or trim with `ResultSelector` and `OutputPath` so only what later states actually read survives.
- Where does a Parallel or Map state typically hit the limit?At fan-in. Parallel returns an array of branch outputs and Map returns one entry per item, so aggregate size scales with branches or items even when each result is modest. Trim each branch's output, or have branches write to S3 and return keys — which is exactly what Distributed Map's ResultWriter automates.
- Do variables let a workflow carry more data than the payload limit allows?No — variables assigned with `Assign` are still bounded, and they do not raise the 256 KiB cap. What they remove is the need to thread a value through every intermediate state just so a later one can read it, which is one of the main causes of payload bloat in long workflows.
saying these in an interview costs you the question
- Thinking the limit applies to the work, not the JSON payload
- Returning a whole file's contents from a Task
- Merging every result with ResultPath and never trimming
- Assuming Parallel and Map outputs are exempt
- Believing the cap is measured in megabytes