"The response was longer than expected and blew past my Cloud Run limit." "My JSON got cut off mid-object." "I asked for a one-line answer and got three paragraphs." If you've shipped anything serious with the Gemini API, output-length issues like these probably feel familiar. The first one that bit me personally was much subtler: the same prompt would return responses of wildly different lengths from one day to the next. The root cause turned out to be confusing thinking tokens with output tokens, which cost me half a day before I caught it.
In this guide, I'll walk through how I use maxOutputTokens and stopSequences together to land response length where I actually want it. The official docs cover each parameter briefly, but the interesting behavior shows up when you combine them — so we'll spend most of our time on that combination, including the production gotchas I've personally hit.
maxOutputTokens is a hard ceiling, not a target
The most common misunderstanding is treating maxOutputTokens as "roughly how long I'd like the answer to be." It isn't. It's the strict upper limit on the number of output tokens the model can produce, and the model will be cut off the moment it hits that ceiling — even mid-sentence, even mid-JSON.
# Python SDK example
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="Summarize the following meeting notes in about 300 characters.\n---\n(meeting notes)",
config=types.GenerateContentConfig(
max_output_tokens=400, # safety margin above the 300-char target
temperature=0.3,
),
)
print(response.text)
print("finish_reason:", response.candidates[0].finish_reason)The most useful habit is always checking finish_reason. STOP means a clean natural ending; MAX_TOKENS means the limit clipped the response. Logging the rate of MAX_TOKENS in production is a small change that pays back in both cost optimization and quality monitoring.
Think in tokens, not characters
For English, roughly one word equals 1.3 tokens, so a 500-word answer needs about 700 output tokens. Japanese tends to run 1–2 tokens per character. If you need precision, call client.models.count_tokens() against your actual prompt before fixing the limit.
# Measure expected input cost up front
count = client.models.count_tokens(
model="gemini-2.5-pro",
contents="Summarize the following meeting notes in about 300 characters.",
)
print(f"input_tokens: {count.total_tokens}")
# Knowing the input side makes total cost estimation accurate.Thinking tokens are billed separately on 2.5 / 3 series models
This is the trap most teams fall into. On Gemini 2.5 Pro and the Gemini 3 series, the model produces internal thinking tokens before the output tokens you see. Those thinking tokens are not counted against maxOutputTokens, but they are billed. So you can set the output ceiling to 500 and still see a single call charge you for 3,000-plus tokens. If cost predictability matters, you also need to budget the thinking side — see Gemini 2.5 thinking budget optimization guide.
stopSequences: telling the model "end here"
stopSequences lets you specify up to five strings; the moment any of them appears in the generated output, the response stops cold.
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Write one sentence using these three words: cat, river, morning. Output one sentence only.",
config=types.GenerateContentConfig(
stop_sequences=["\n\n", "###", "Explanation:"],
max_output_tokens=200,
temperature=0.7,
),
)
print(response.text)This stops the moment Gemini hits a double newline, a ###, or the word Explanation:. Gemini has a habit of helpfully adding "Here's why I picked those words..." after a one-sentence answer; stopSequences is the cleanest way to suppress that tendency.
The stop string itself is dropped from the response
A subtle but important detail: the stop string does not appear in the returned text. If you tell the model to wrap its reply in <answer>...</answer> and set stop_sequences=["</answer>"], the closing tag will be missing from the response. Downstream code that assumes the closing tag is present will break. I've personally had a parser blow up on this.
Watch out when combining with JSON mode
If you're using response_mime_type="application/json" for structured output, do not put a JSON syntax character like } in stopSequences — you'll truncate to an empty object. For structured output, prefer a Pydantic schema via responseSchema rather than ad-hoc stop strings.
Combining both: production patterns I actually use
In real systems, I use the two together as a defense-in-depth setup: maxOutputTokens is the safety valve, stopSequences is the intent expression.
Pattern 1: Strict-length summarization
def summarize_strict(text: str, max_chars: int = 200) -> dict:
"""Return a summary roughly within max_chars; flag it if truncation occurred."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=(
f"Summarize the following text in {max_chars} characters or fewer. "
f"Return only the summary; no preamble.\n---\n{text}"
),
config=types.GenerateContentConfig(
max_output_tokens=int(max_chars * 1.5), # safety margin
stop_sequences=["\n\nSummary", "\n\nIn closing", "###"],
temperature=0.2,
),
)
finish = response.candidates[0].finish_reason.name
return {
"text": response.text.strip(),
"truncated": finish == "MAX_TOKENS",
"finish_reason": finish,
}
result = summarize_strict("(long article body)", max_chars=200)
if result["truncated"]:
print("⚠️ Hit the ceiling — review the prompt's character target.")
print(result["text"])Pattern 2: Lifting structured short answers
When you want fixed-format output like Category: <value>, ending the response at the structural boundary makes parsing reliable.
PROMPT = """Classify the following inquiry using this format:
Category: <technical|billing|sales|other>
Reason: <30 chars or fewer>
---
Inquiry: {query}"""
def classify(query: str) -> dict:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=PROMPT.format(query=query),
config=types.GenerateContentConfig(
max_output_tokens=80,
stop_sequences=["\n---", "\n\n"],
temperature=0.0,
),
)
out = response.text
cat = out.split("Category:")[-1].split("\n")[0].strip()
reason = out.split("Reason:")[-1].strip() if "Reason:" in out else ""
return {"category": cat, "reason": reason}
print(classify("I can't log in"))
# → {'category': 'technical', 'reason': 'Login-related issue'}Using stopSequences as a parsing anchor like this makes downstream code much steadier. If the model isn't stopping where you expect, my recommended order is: first tighten the prompt with explicit "do not output anything outside the format" instructions, then add stopSequences as a backstop. Doing it in that order keeps the prompt the source of truth.
Pattern 3: Defensive defaults for shared SDK wrappers
If you're building a thin wrapper around the SDK that other developers in your team will call, set defensive defaults so the worst-case caller doesn't blow your bill or your latency budget.
DEFAULTS = {
"max_output_tokens": 1024,
"temperature": 0.4,
"stop_sequences": [],
}
def call_gemini(prompt: str, **overrides) -> dict:
cfg = {**DEFAULTS, **overrides}
response = client.models.generate_content(
model=cfg.pop("model", "gemini-2.5-flash"),
contents=prompt,
config=types.GenerateContentConfig(**cfg),
)
return {
"text": response.text,
"finish_reason": response.candidates[0].finish_reason.name,
"input_tokens": response.usage_metadata.prompt_token_count,
"output_tokens": response.usage_metadata.candidates_token_count,
"thinking_tokens": getattr(response.usage_metadata, "thoughts_token_count", 0),
}Returning usage_metadata alongside the text — particularly the thinking-token count on 2.5/3 series models — gives every caller the data they need to track real cost without re-instrumenting individually.
Pitfalls to watch out for
A few mistakes I see (or have made myself) repeatedly:
- Streaming feels different: with streaming responses, hitting a stop sequence cuts the stream off immediately, so you may need a UI affordance for "this ended sooner than expected." For the broader streaming control patterns, see Streaming response chunk control.
- Long inputs squeeze the output budget: the model's total context window covers input + output + thinking. Pump in a million tokens and there's effectively nothing left for output. For long-document summarization, either chunk the input or do a Flash-then-Pro two-pass.
- Silently swallowing finish_reason: if you don't surface
MAX_TOKENS, you get the "responses are mysteriously short some days" bug. The first thing to check when output feels short is alwaysfinish_reason. The deep dive on truncation is in Troubleshooting truncated responses. - Stop sequences and Unicode quirks: if your stop sequence contains a smart quote or a curly apostrophe and the model produces straight quotes, the match fails silently. Test stop sequences with a couple of generations before deploying, and prefer ASCII-only delimiters when possible.
- Production observability is non-optional: I always log
usage_metadata(input, output, thinking) along withfinish_reasonto a structured log. After a week of data, you can plot the distribution of output token counts per endpoint and immediately see which calls are wasting tokens versus getting clipped. That's the data that turns guesswork into evidence.
Wrapping up — the next step worth taking
Treat maxOutputTokens as a safety valve and stopSequences as intent expression, and most output-length issues become tractable. If you try one thing from this article, add finish_reason logging to your most-used Gemini call. After watching the logs for a week, you'll have a clear view of which calls are clipping at MAX_TOKENS, and you can adjust ceilings from real data instead of guesswork. That single change has saved me more debugging time than any prompt tweak.
If you want to go deeper on the prompt-side of output predictability, the chapters on instruction granularity in Prompt Engineering: A Practical Approach (or any of the recent reliable production-LLM guides) pair well with this hands-on tuning. The combination of API-side controls and prompt-side discipline is what actually moves output behavior from "mostly right" to "predictably right."