Messages API Best Practices
A collected set of practices for structuring roles, turns, content blocks, and system prompts reliably when building against the Messages API.
Treat this as a working reference to check your code against, not a one-time read.
How to Use This List
- Skim the category headers first, then dive into the section relevant to what you're building.
- Each rule states the practice positively - do this - along with why it matters.
- Revisit this list whenever you're debugging a 400 error, an inconsistent reply, or unexpected token costs - most issues trace back to one of these.
A - Roles and Turns
- Always start the messages array with a role of "user". The API rejects any array that begins with
assistant, so build conversations from the first user input forward. - Keep roles strictly alternating. Never send two consecutive
userorassistantturns - combine multiple pieces of user input into one turn's content instead of splitting them across separate array entries. - Append the full
response.content, not just extracted text, as the next assistant turn. This preserves tool_use blocks and any other structured content needed for the conversation to continue correctly. - Treat the API as fully stateless. Never assume Claude remembers a previous call - resend the complete relevant history on every request.
- Scope one messages list per conversation. Never share a single mutable list across concurrent users or sessions; interleaved turns from different conversations will break alternation.
B - System Prompts
- Keep the system prompt as a separate, stable string. Don't fold standing instructions into a
userturn -systemis the correct place for persona, scope, and constraints that apply to the whole conversation. - Structure long system prompts into clear sections. Persona, scope, constraints, and output format as distinct sections are easier to audit and update than one undifferentiated paragraph.
- Avoid interpolating dynamic values into the system prompt. Timestamps, request IDs, or user IDs embedded in
systemmake it a different string on every request, silently breaking prompt caching. - Resend the same system prompt on every request in a conversation. Unlike
messages,systemis not cumulative - pass the identical value each time you want consistent behavior. - Use mid-conversation system messages for updates, where supported. On models that support it, appending a
role: "system"message preserves your cached prefix better than rewriting the top-levelsystemfield mid-conversation.
C - Content Blocks
- Match content block type to content type. Use
textfor prose,imagefor visual input,tool_usefor a model-initiated tool call, andtool_resultfor the answer to one - don't force everything into plain strings. - Always parse tool_use input as structured data. Read
block.inputas the parsed object it is; never string-match the serialized JSON, since exact escaping can vary. - Return every tool_result together in a single user turn when an assistant turn requested multiple tools. Splitting them across separate turns breaks the expected pairing and can suppress future parallel tool calls.
- Match every tool_use with its tool_result by tool_use_id. Never drop a result for a pending tool call, even a failed one - use
is_error: trueinstead of omitting it. - Check a content block's type before accessing type-specific fields. Don't assume every block in a response is
text- branch onblock.typefirst, sinceassistantturns can includetool_usealongside or instead of text.
D - Conversation State and Context
- Measure token usage with
count_tokens(), not character-count estimates. Token counts don't map linearly to string length, especially for code or non-English text. - Trim or summarize proactively, before hitting a context-window failure. Check usage against a safety threshold below the actual limit rather than reacting to an error in production.
- Trim in matched pairs. If you drop an assistant turn containing
tool_use, drop the corresponding user turn'stool_resulttoo - never leave one half of a pair orphaned. - Use a cheaper, faster model for summarization steps. Summarizing older turns doesn't need your primary model's full capability - Claude Haiku 4.5 is typically sufficient and reduces cost.
- Verify a trimmed or summarized array still starts with "user" and alternates correctly. Slicing an array can leave it starting with
assistantor with adjacent same-role turns; fix this before sending.
E - Errors and Reliability
- Catch the SDK's typed exceptions, from most specific to least specific. Distinguish
RateLimitError,NotFoundError, andAuthenticationErrorfrom the broaderAPIStatusErrorinstead of catching one generic exception class. - Always check
stop_reasonbefore trusting a response is complete. Astop_reasonofmax_tokensmeans the reply was cut off, not finished naturally. - Rely on the SDK's built-in retry behavior for 429 and 5xx errors. Don't hand-roll retry logic for transient failures the client already retries with backoff by default.
- Stream requests with a large
max_tokens. Non-streaming requests with a high token ceiling risk hitting client-side HTTP timeouts; streaming avoids that failure mode. - Set
max_tokensdeliberately, not as an afterthought. Too low truncates output mid-thought and forces a retry; size it to the task, with headroom for thinking or tool calls where relevant.
FAQs
What's the single most common cause of a 400 error on the Messages API?
Broken role alternation - either the array doesn't start with user, or two same-role turns appear back to back. Check the structure before checking the content.
Why does resending the system prompt on every request matter?
Because system isn't cumulative like messages - each request is evaluated independently, so omitting it or changing it silently shifts behavior and can invalidate a prompt cache built on the prior text.
Should I ever skip appending a tool_result for a tool_use block?
No - every tool_use block needs a matching tool_result, even if the tool call failed. Use is_error: true for a failed call rather than omitting the result entirely.
Is it safe to estimate token usage from string length instead of calling count_tokens?
No - token counts don't map linearly to character or word counts, especially for code or non-English text, so estimates can be significantly off in either direction.
Why use a cheaper model for summarizing old conversation turns?
Because summarization is a comparatively simple task that doesn't need your primary model's full reasoning capability - a faster, cheaper model reduces the cost of trimming without a meaningful quality loss.
What should I check before trusting that a response is complete?
The stop_reason field - if it's max_tokens rather than end_turn, the reply was cut off and may be missing content you expect to be there.
When should I switch to streaming instead of a normal request?
Whenever max_tokens is set high enough that a non-streaming request risks a client-side HTTP timeout - streaming avoids that failure mode entirely.
Why does mixing string-matching and tool_use input parsing cause bugs?
Because the exact JSON serialization of tool input can vary in escaping between requests or models - always parse block.input as structured data rather than pattern-matching the raw text.
Is it okay to share one messages list across multiple users' conversations?
No - a shared, mutable list used by concurrent conversations will interleave turns from different users and break the required role alternation for all of them.
What's the risk of interpolating a timestamp into the system prompt?
It makes the system prompt a different string on every request, which silently breaks prompt caching since caching relies on a stable, unchanged prefix.
Do I need to manually implement retry logic for rate limits?
Generally no - the official SDK retries 429 and 5xx responses automatically with backoff by default, so hand-rolled retry logic is usually unnecessary unless you need custom behavior.
Related
- Understanding Roles and Turns in the Messages API - the conceptual foundation behind the role and turn practices here.
- Building Multi-Turn Conversations with the Messages Array - detailed mechanics behind the conversation-state practices.
- Managing Conversation History and Trimming for the Context Window - the full trimming and summarization strategies referenced in category D.
- Handling Mixed Content Blocks: Text, Image, and Tool Results in One Turn - worked examples behind the content-block practices in category C.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.