Claude stop_reason max_tokens: 83 Reports Silently Cut Off Mid-JSON A developer running Preterview, an interview-prep platform, found that 83 Claude-generated reports were silently truncated because the code never checked the API's stop_reason field, which returned "max_tokens" when output hit the 1,500-token ceiling. A json_repair helper masked 79 of the failures by closing truncated JSON, so error monitoring showed only 4 hard failures over three weeks; counting stop_reason in stored logs revealed the true rate was 20 times higher, and four fixes brought truncations to zero. The support email was two lines long: "My report stops at question 6. There were 9 questions. Also there's no 'next steps' section?" I opened the report. It looked fine. Clean formatting, scores, written feedback, no error banner. It just ended early, like a book with the last three chapters torn out and the cover glued back on. The cause turned out to be one field I never read: Claude's stop reason . When it says max tokens , the model didn't finish. My code didn't care, and a JSON repair helper made sure nobody else would notice either. This is the autopsy of how Claude stop reason max tokens silently truncated 83 reports, and the four changes that took it to zero. stop reason == "max tokens" , the output is incomplete. Always check it before parsing anything. max tokens costs nothing by itself. You pay for tokens generated, not for the ceiling. The system is the report generator inside Preterview, an interview prep platform I built and run full disclosure: I built it . It runs voice interviews with three interviewer styles, then sends the full transcript to Claude and asks for a structured written report: per-question feedback, rubric scores, and next steps. You can see what a session looks like at preterview.com/en https://preterview.com/en . The report call is the one place where output length actually varies a lot. A 5-question session produces a short report. A 9-question session with a chatty candidate produces a long one. I set max tokens=1500 back when every session had 5 questions, and then I forgot that number existed. stop reason: "max tokens" means Claude stopped because it hit the max tokens limit you set, not because it was done. The text you get back is a prefix of what the model intended to write. For prose, that's an abrupt ending. For JSON, it's a document with unclosed brackets. The happy value is "end turn" : the model finished on its own. There are others "tool use" , "stop sequence" , but for a plain "write me a JSON report" call, anything other than end turn deserves suspicion. Here's roughly what my original code looked like: resp = client.messages.create model=MODEL, max tokens=1500, system=REPORT SYSTEM PROMPT, messages= {"role": "user", "content": transcript block} , raw = resp.content 0 .text data = json.loads repair json raw "just in case" report = Report.model validate data Notice what's missing. Nothing reads resp.stop reason . And that repair json call is the real villain. Because I had wrapped the parser in a JSON repair helper the json repair package , added months earlier to survive the occasional trailing comma or stray markdown fence. Repair libraries are built to turn broken JSON into valid JSON. A truncated document is just broken JSON with a missing ending, so it closes the open strings, arrays and objects, and returns something parseable. The truncated output looked like this at the tail: {"q": 6, "feedback": "Strong STAR structure, but the result was vague. Next time quantify the" After repair, question 6 had a feedback string ending mid-sentence, questions 7 to 9 didn't exist, and the next steps array was gone. Then Pydantic validated it, because I'd given next steps a default of and made per-question feedback a list of any length. Every layer did its job. The result was a confidently wrong report. Out of 83 truncated responses, the repair helper "fixed" 79. Only 4 failed hard, and those were the ones that died mid-key in a way the repairer couldn't guess. So my error monitoring showed 4 failures in three weeks, which looked like noise. The real failure rate was 20 times higher. I found them by counting stop reason in my logs, which I was storing all along and never reading. I log the raw API response metadata for every call, so a single query over three weeks gave the answer: | Session length | Reports | stop reason = max tokens | |---|---|---| | 5-7 questions | 928 | 0 | | 8-9 questions | 212 | 83 39% | | Total | 1,140 | 83 7.3% | That split is the whole story. The 1,500-token cap was sized for the product I launched, not the product I was running. When I added longer sessions, the report grew past the cap for about 4 in 10 long sessions, and nothing in the pipeline was designed to say so. The users hit hardest were the ones who did the longest, most serious practice sessions. The people most invested in the product got the worst reports. That one stung. Check stop reason before you parse, size max tokens for the worst case, order your schema so critical fields come first, and only repair output that finished cleanly. I shipped all four. 1. Gate on stop reason. resp = client.messages.create model=MODEL, max tokens=4096, ... if resp.stop reason = "end turn": raise TruncatedOutput f"stop reason={resp.stop reason}, " f"output tokens={resp.usage.output tokens}" raw = resp.content 0 .text data = json.loads raw TruncatedOutput triggers a retry path more on that below and fires an alert. A truncated report is now a loud failure instead of a quiet lie. 2. Raise the cap. It's free. I'd been treating max tokens like a budget. It isn't. You're billed for output tokens actually generated, so a cap of 4,096 on a report that needs 1,100 tokens costs exactly what a cap of 1,500 does. The cap is a safety rail against runaway output, not a cost control. I picked 4,096 because my longest observed report afterward was 2,350 tokens at p99, which leaves real headroom. 3. Reorder the schema so scores come first. Claude writes JSON top to bottom. Whatever sits at the end of your schema is what gets cut when you run out. My original order was summary, per-question feedback, scores, next steps. The scores, the part users screenshot and compare week to week, were in the danger zone. New order: scores , summary , next steps , then per question last. If something ever truncates again, the user loses detail on the final question, not their score. 4. Repair only clean finishes. I kept repair json , but it only runs when stop reason == "end turn" and json.loads fails. Then it's fixing formatting slop from a complete answer, which is what it's good at. It never touches a response that was cut off. It gets retried as two smaller calls instead of one bigger one. The retry path asks for scores, summary and next steps in the first call, then per-question feedback for the second half of the questions in a separate call, and merges them. Each half fits comfortably in the cap, and the split adds about 4 seconds, which is fine for a report users read after the session ends. So far the retry path has run 0 times. Over the next 1,300 reports after the fix, stop reason was end turn on every single one. The split call exists because "it can't happen" is exactly what I believed about the original bug. Total output tokens went up about 5%, entirely because the 8-9 question reports now finish instead of stopping early. Latency on long reports rose by roughly the time it takes to generate the missing tail. No change on short sessions. I also emailed the 61 users behind the 83 truncated reports and regenerated their reports from stored transcripts, which took one script and an awkward apology paragraph. stop reason and usage.output tokens for every call. I had the data for three weeks and it would have told me on day one. Claude returned stop reason: "max tokens" on 83 of 1,140 reports because a 1,500-token cap set for 5-question sessions couldn't fit 8-9 question reports, and my code never checked stop reason . A JSON repair helper then closed the brackets on the truncated output, so the broken reports parsed, validated and shipped without an error. The fix: reject any response where stop reason isn't end turn , raise max tokens it costs nothing until used , put critical fields first in the schema, and only repair JSON from responses that finished cleanly. Written by the developer behind Preterview https://preterview.com/en , an interview prep platform.