Technical fundamentals: CCDV-F study guide
CCDV-F · Model Selection and Optimization, topic weight 6.1% of the exam
Technical Fundamentals is a 6.1% topic in Model Selection and Optimization, which makes up 16.8% of the CCDV-F exam. It tests the plumbing under every Claude application: what the client SDKs do on top of the REST API, how to handle each error type, and how streaming reaches a user's screen.
What the official guide covers
The Claude Certified Developer Foundations exam guide (version 1.0, effective July 2026) describes this topic as foundational technical concepts, including basic engineering practices such as "integrating with SDKs that wrap REST APIs, websockets".
| What the guide lists | What it means in practice |
|---|---|
| SDKs that wrap REST APIs | What the Claude SDK adds to raw HTTP: headers, retries, timeouts, typed errors, request IDs, streaming helpers |
| Websockets | How the Claude API's server-sent events differ from WebSockets, and where each belongs in your app |
| Basic engineering practices | Retrying only what can succeed, setting timeouts, logging request IDs, keeping keys on the server |
The REST call under the SDK
The Claude API is plain HTTPS and JSON. This is a complete request:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model": "'"$CLAUDE_MODEL"'", "max_tokens": 256,
"messages": [{"role": "user", "content": "Hello, Claude"}]}'
Every response carries a request-id header. Errors come back as JSON with a type of "error", an error object holding its own type and message, and the request_id.
Official client SDKs exist for Python, TypeScript, Java, Go, C#, Ruby and PHP. They send the same requests; the difference is what you no longer write yourself.
What the SDK adds
| Concern | With raw HTTP | With the Python SDK (pip install anthropic) |
|---|---|---|
| Auth and version headers | You set them | Reads ANTHROPIC_API_KEY and sets the headers |
| Retries | You write backoff | Retries connection errors, 408, 409, 429 and 5xx errors twice by default; change with max_retries |
| Timeouts | You choose one | 10 minutes by default; set timeout on the client or per request with client.with_options(...) |
| Errors | Parse the body | Typed exceptions such as BadRequestError, AuthenticationError, RateLimitError, InternalServerError and APIConnectionError |
| Request ID | Read the header | response._request_id |
| Streaming | Parse server-sent events yourself | client.messages.stream(), text_stream, get_final_message() |
| Async | Your own HTTP client | AsyncAnthropic with the same methods; in TypeScript the single client is Promise-based, so you await it |
Error types and what to do with each
| Status | Error type | Retry? | What to do |
|---|---|---|---|
| 400 | invalid_request_error | No | Fix the request; the same request fails again |
| 401 | authentication_error | No | Fix or rotate the key |
| 403 | permission_error | No | The key lacks access to that resource |
| 404 | not_found_error | No | Check the model ID or resource ID |
| 413 | request_too_large | No | Shrink or split the request |
| 429 | rate_limit_error | Yes, unless it is a spend cap | Wait for the retry-after header; smooth bursts and ramp traffic up gradually. A spend-cap 429 has no retry-after and keeps failing until access resumes |
| 500 | api_error | Yes | Retry with exponential backoff |
| 504 | timeout_error | Yes | Retry; if it repeats on the same large request, stream it or make it smaller |
| 529 | overloaded_error | Yes | Retry with backoff |
The API also enforces acceleration limits when an organisation's usage rises sharply, which is another reason to ramp traffic up rather than switch it on all at once.
Retriable or terminal: one question
For any failure, ask: would the identical request plausibly succeed if sent again later? If yes, it is retriable (rate limits, overload, timeouts, dropped connections). If no, it is terminal (a malformed body, a bad key, a missing model), and every retry wastes time and budget while hiding the real fault. When you cannot tell, treat it as terminal: a wrong terminal call fails loudly and gets fixed, while a wrong retriable call hammers the service.
Two more traps sit next to this decision:
- One retry layer, not two. The SDK already retries connection errors, 408, 409, 429 and 5xx responses. A hand-written loop around the same call multiplies attempts against a rate limit. Either keep the SDK's retries and add only application fallbacks, or set
max_retries=0and own the whole path. - A refusal is not an error. It arrives as HTTP 200 with
stop_reason: "refusal", so no retry logic sees it. Checkstop_reason, log the refusal and return it to the caller rather than retrying or treating it as valid output.
Worked example: owning the retry path
A common broken helper catches every exception and retries at once with no wait. It retries 400s that can never succeed and hammers a rate limit it should wait out. When you need your own policy, turn the SDK's retries off so there is only one layer, then classify, wait and give up:
import random
import time
import anthropic
client = anthropic.Anthropic(max_retries=0) # this function is the only retry layer
RETRIABLE = {408, 409, 429, 500, 502, 503, 504, 529}
def create_with_retry(max_attempts=5, cap=30, **params):
for attempt in range(1, max_attempts + 1):
try:
return client.messages.create(**params)
except anthropic.APIStatusError as e:
if e.status_code not in RETRIABLE or attempt == max_attempts:
raise # terminal, or out of attempts
wait = e.response.headers.get("retry-after")
except anthropic.APIConnectionError: # dropped connection or timeout
if attempt == max_attempts:
raise
wait = None
delay = float(wait) if wait else min(cap, 2 ** attempt) + random.uniform(0, 1)
time.sleep(delay) # server's wait first, then capped backoff with jitter
The attempt cap matters as much as the wait: a spend-cap 429 has no retry-after and will not clear within a job, so the loop must end and report it.
Server-sent events and WebSockets
The Claude API streams with server-sent events (SSE). Your client makes one HTTP request with stream: true, and the server pushes events one way down that response: message_start, content block events, message_delta, message_stop, with ping events in between. There is no WebSocket connection to the Claude API.
WebSockets are two-way and long-lived. They belong between your users and your server, when the browser needs to send as well as receive: a chat that can be interrupted, or an agent the user steers. Anthropic's Agent SDK hosting guide describes the same split: the container exposes an HTTP or WebSocket endpoint, and the agent subprocess itself does not listen on the network.
The usual shape is: browser to your server over a WebSocket, your server to Claude over an SSE stream. The API key stays on the server.
Worked example: relay a Claude stream over a WebSocket
import os
import anthropic
from anthropic import AsyncAnthropic
from fastapi import FastAPI, WebSocket
app = FastAPI()
client = AsyncAnthropic() # the API key never reaches the browser
MODEL = os.environ["CLAUDE_MODEL"]
@app.websocket("/chat")
async def chat(ws: WebSocket):
await ws.accept()
history = []
while True:
history.append({"role": "user", "content": await ws.receive_text()})
try:
async with client.messages.stream(model=MODEL, max_tokens=1024, messages=history) as stream:
async for text in stream.text_stream: # SSE from Claude
await ws.send_json({"type": "delta", "text": text})
final = await stream.get_final_message()
except anthropic.RateLimitError: # raised after the SDK's own retries
history.pop()
await ws.send_json({"type": "error", "message": "Busy. Please try again shortly."})
continue
except anthropic.APIError: # includes errors sent mid-stream
history.pop()
await ws.send_json({"type": "error", "message": "The request failed."})
continue
history.append({"role": "assistant", "content": final.content})
await ws.send_json({"type": "done", "stop_reason": final.stop_reason})
Two details matter. An error can arrive as an SSE error event after the HTTP status was already 200, so the relay must report failures inside the stream, not only before it. And stop_reason tells the browser whether the reply finished (end_turn) or was cut off (max_tokens).
Decisions the exam tests
| Situation | Choose | Why |
|---|---|---|
| Browser chat with live text and a stop button | WebSocket to your server, SSE from your server to Claude | Two-way for the user; the key stays server-side |
| Server-to-server call with a short reply | A plain SDK call | Simplest; nothing to relay |
A reply with a very large max_tokens | Streaming | The SDK requires it to avoid HTTP timeouts |
| Requests that may run over 10 minutes | Streaming or the Message Batches API | Idle connections can be dropped on long non-streaming calls |
| Bursty traffic hitting 429 | SDK retries plus your own queue or concurrency cap | Retries absorb spikes; the cap prevents them |
| An unexplained bad response in production | Look up the logged request ID | It is what Anthropic support asks for |
Rules that decide exam answers
- Retry only what can succeed, in one layer. 429, 500, 504, 529 and connection errors are retryable. 400, 401, 403, 404 and 413 fail again until you change something. Do not wrap your own retry loop around the SDK's.
- Honour
retry-after. On a 429, wait as long as the header says. Retrying at once makes it worse. - Claude streams over SSE. WebSockets sit between your users and your server, not between your server and Claude.
- Keep the API key on the server. A browser never calls the Claude API directly with your key.
- A 200 can still fail. In a stream, handle
errorevents and readstop_reasonat the end. - Log the request ID. Every response has one, and support needs it.
Where it appears in the exam
Technical Fundamentals is 6.1% of the exam, inside Model Selection and Optimization (16.8%), so expect about three of the 53 items. The guide names SDKs that wrap REST APIs and WebSockets, so questions describe an integration that fails, retries badly, times out or streams to the wrong place, and ask for the sound engineering fix.
Two sample questions
These are original Timo practice questions. They are not official exam questions.
Build exercise
- Send the
curlrequest above, then the same request with the Python SDK. Compare the headers and printresponse._request_id. - Send a request with no
max_tokens, catch theBadRequestErrorand print its status code and message. - Add
"stream": trueto thecurlbody and match each event in the raw output to the list above. - Run the WebSocket relay locally with
uvicorn, open the chat from two browser tabs and confirm each tab streams its own reply.
Practise this topic
- Claude Certified Developer practice exam: free, 20 questions, no sign-up
- CCDV-F study guide: all topics
- Previous topic: LLM Fundamentals
- Next topic: Model Selection and Tradeoffs
Sources
- Claude Certified Developer Foundations Exam Guide, version 1.0, effective July 2026 (Anthropic), Domain 5 topic: Technical Fundamentals
- Anthropic documentation: Python SDK
- Anthropic documentation: Claude API errors
- Anthropic documentation: Streaming messages
- Claude Agent SDK documentation: Hosting the Agent SDK
By Amotion AI