OpenAI's Stop Parameter: Four Sequences, Then What
8 min read · updated August 11, 2026
The stop parameter accepts up to four sequences. A fifth is not ignored and not truncated — the request is rejected before generation begins, with a 400 and an error object naming the parameter.
The cap is four
OpenAI’s Chat Completions API reference documents stop as a string or an array of up to four strings, and describes the behaviour as: the API stops generating further tokens when one of them is produced. Four is the documented maximum and has been for as long as the parameter has existed.
Both forms are valid, and the single-string form is not a special case with different behaviour — it is the array of one, spelled shorter:
"stop": "\n\n" "stop": ["\n\n", "END", "###", "Human:"]
There is no version of the parameter that takes more, and no header or flag that lifts the cap. If your design needs eight stop conditions, the design needs to change rather than the request.
stop is a Chat Completions parameter. OpenAI’s newer Responses API has its own parameter set, and the reasoning models restrict several sampling parameters that older models accept. Check the reference for the endpoint and model you are calling rather than assuming the parameter carries across.What a fifth sequence returns
Send five and you get HTTP 400 with OpenAI’s standard error envelope. The envelope is the stable part and is worth knowing field by field, because every parameter validation failure in this API has the same shape:
HTTP/1.1 400 Bad Request
{
"error": {
"message": "Invalid 'stop': array too long. Expected an array with
maximum length 4, but got an array with length 5 instead.",
"type": "invalid_request_error",
"param": "stop",
"code": "array_above_max_length"
}
}type—invalid_request_errorfor anything wrong with what you sent, as opposed torate_limit_error,authentication_erroror a server-side failure. This is the field to branch on: aninvalid_request_errorwill fail identically on retry, so retrying it is always wrong.param— the offending parameter, herestop. Present on validation errors and null on others. This is what lets a client log something more useful than “400 from OpenAI”.code— a machine-readable label for the specific violation.message— human-readable. Useful in a log, and not something to match on.
message string above is representative wording, not a contract. OpenAI rewords these, and a client that branches on message text will break silently on a wording change. Branch on type, param and code; log message.The important operational point is that this is a pre-generation failure. No tokens are produced, nothing is billed, and there is no partial response to salvage. It is a bug in your request, and it will appear the first time you run the code path — which is a much better failure than a limit that silently drops the fifth sequence and lets you ship a model that never stops where you expected.
The stop text is not in the output
When a stop sequence fires, the sequence itself is excluded from content. Generation halts at the boundary and what you get back is everything before it.
request: "stop": ["\nUser:"] model produced: "Sure, here is the answer.\nUser:" content returned: "Sure, here is the answer." finish_reason: "stop"
This is the behaviour you want for the common use — trimming a conversational transcript at the next speaker label — and it is a trap if you are using stop sequences as delimiters in structured output, because the delimiter you were going to split on is gone. If you need to know which of four sequences fired, you cannot tell from the content. You cannot tell from finish_reason either: it is "stop" for a stop sequence and also "stop" for the model finishing naturally at its own end-of-turn token. The two are indistinguishable in the response.
If the distinction matters, the workable approach is to make the stop sequences mutually exclusive by construction — one per branch of the logic — so that whichever one fired is inferable from what the content ends with, or to drop stop sequences and use a response schema so the boundary is structural rather than lexical.
Why it matches tokens, not characters
Stop sequences are strings in the API, but the model generates tokens, and the match happens against the generated text as it accumulates. A stop string that does not align to a token boundary can behave in ways that look inconsistent: the model may produce a token that contains the stop string plus trailing characters, and the exact trimming behaviour at that boundary is an implementation detail rather than a documented guarantee. This is why single-character stop sequences and stop sequences inside common words are unreliable, and why a distinctive multi-character delimiter — "###END###" rather than "." — is the robust choice.
It is also why a stop sequence cannot prevent the model from starting to say something. The check happens after tokens are generated. You are trimming output, not steering it, and you are billed for the tokens produced up to and including the one that triggered the stop. The multi-token case is worked through in multi-token stop behaviour.
What to use when four is not enough
- Find the common prefix. Four sequences go a long way if they are chosen well.
"\nUser:","\nHuman:","\nQ:"and"\nAssistant:"collapse to fewer if the real boundary is a newline followed by a capitalised label — pick the newline-plus-label pattern that actually occurs in your data rather than enumerating every label you can imagine. - Move the boundary into the schema. With Structured Outputs the model produces a JSON object and stops when the object is complete. There is nothing to delimit, so there is nothing to stop on.
- Post-process instead. Stop sequences save output tokens; they do not do anything a truncation in your own code cannot do afterwards. If you have nine markers, generate and cut. The cost is the tokens between the first marker and the end, which for short completions is negligible.
- Cut the stream client-side. If you are streaming, you can close the connection when your own matcher fires, against as many patterns as you like. You still pay for what was generated before you noticed, but the user stops seeing it immediately. The chunk structure to match against is in the streaming chunk format.