The first time Anthropic contacted me about a suspicious request pattern on my small chat app, I froze. I could not answer their question of "which user is this from" because I had never been sending metadata.user_id on my requests, and there was no way to map an Anthropic-side observation back to a row in my own database.
The metadata.user_id field is a tiny, optional parameter on the Messages API. Public documentation says you can pass a per-user ID, and stops there. What is missing is a clear explanation of what the field is actually for, and what changes when you start sending it. In this piece I want to share the operational lessons I picked up running a multi-user app on the Claude API, and the implementation pattern I have settled on.
metadata.user_id is designed as a hint for abuse detection
If you read the API reference together with Anthropic's usage policies, the role of metadata.user_id becomes clear. It is a signal that lets Anthropic, on detecting an unusual request pattern, separate "a problem affecting your whole service" from "a problem caused by one specific user."
Without this field, Anthropic only sees your API key. If a single user generates a flood of inappropriate prompts, that behavior accumulates against your account, and in the worst case your entire API key can be subject to restrictions. Pass a user_id and Anthropic can isolate the issue: "only requests carrying this user_id are problematic," which lets them respond at the right granularity instead of penalizing the whole account.
In other words, this is a field for protecting your own account, not a field for analytics or user tracking. Treat it as an insurance policy you take out on behalf of your service.
What changes when you start sending it — observed in production
I ran my own small chat app (a few thousand requests per month) for one month with user_id and one month without, and the wording of the notifications I received from Anthropic was visibly different.
In the period without user_id, the email body said something abstract like "we have observed certain patterns in your service overall," which forced me to read through every request log on my side to find the cause. In the period with user_id, the same kind of notification included the actual hashed user_id value, and I could match it against my own database in seconds.
There is a secondary effect as well. Per-user rate-limit decisions on Anthropic's side appear to use user_id as a key, so when one user briefly bursts you reduce the chance that other users get caught in the same 429 wave. It is hard to feel this difference on a tiny app, but once you cross a few hundred concurrent users it becomes noticeable.
Never send raw email addresses or raw UUIDs
Here is the trap I personally fell into. In my first implementation, I dropped the user's email address straight into metadata.user_id. Two things go wrong with this.
First, you are now transmitting personally identifiable information to a third party that does not strictly need it. Depending on your privacy policy and your jurisdiction, this can quietly take you outside the boundaries of user consent. Second, the value sitting inside Anthropic's logs is now a real, readable email address — clearly fragile in the case of any hypothetical incident.
The recommended approach is to send a stable identifier that contains no PII and is generated entirely on your side. The pattern I use is straightforward: take the internal user ID, run it through HMAC-SHA256 with a server-held secret, and pass the first 16 hex characters.
# anthropic_user_id.py
import hashlib
import hmac
import os
# Fixed salt held in env vars; rotate only with care.
USER_ID_SALT = os.environ["ANTHROPIC_USER_ID_SALT"]
def to_anthropic_user_id(internal_user_id: str) -> str:
"""
Convert an internal user ID into a stable pseudo-ID via HMAC.
The same internal_user_id always produces the same output, so
Anthropic can still attribute requests to a single user.
"""
digest = hmac.new(
USER_ID_SALT.encode("utf-8"),
internal_user_id.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return digest[:16] # 16 hex chars give plenty of uniqueness
# Expected: same input always yields the same 16 chars
# >>> to_anthropic_user_id("user_42")
# '8a3f1c9b2e4d6f01'The critical property is that the hashed value is deterministic for a given user. Anthropic can only group requests as "the same user" if you give them a value that does not change between calls, so derive the pseudo-ID deterministically from your internal ID rather than minting a fresh random value each time.
Treat the salt as something you decide on once before launch and then leave alone. Rotating it is acceptable as a response to an actual leak, but you should accept that the linkage to historical Anthropic-side records will break at the rotation point, and plan operations around that.
Cases where I deliberately do not send metadata
Sending user_id is not always the right answer. In my own work I omit it in these cases:
- CLI tools and local developer tooling: one process equals one user, so the API-key-level granularity is already correct
- Internal admin tools: the user base is small and known, and per-user separation on Anthropic's side adds little value
- Use cases where anonymity is part of the contract: for example, advice or support apps where I do not even want a user_id sitting in my own logs
For consumer-facing web services with an unbounded user base, the safe default is to send the field. For everything else, it is worth deciding deliberately at the start of the project whether you are sending user_id "as defensive insurance" or omitting it "for stated privacy reasons" — making the call up front prevents the policy from drifting later.
Wiring it into a Messages API call
The actual code is short. Pass the hashed value through the metadata parameter and you are done.
import anthropic
from anthropic_user_id import to_anthropic_user_id
client = anthropic.Anthropic()
def chat(internal_user_id: str, prompt: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
metadata={
# Pass the stable, hashed pseudo-ID, never raw PII.
"user_id": to_anthropic_user_id(internal_user_id),
},
messages=[
{"role": "user", "content": prompt},
],
)
return response.content[0].text
# Example
# >>> chat("user_42", "What is the weather today?")
# 'If you can share your location, ...'For error handling and how to react to each stop_reason value, my earlier piece on reading stop_reason on the Claude API covers the patterns I use. If you are also trying to drive down cost, diagnosing prompt-cache misses on the Claude API walks through the same kind of operational debugging on the caching side.
A note on rotating the salt
When I added the salt-based hashing the first time, I made the mistake of treating it like an ordinary application secret and rotating it on the same schedule as my other tokens. The result was that, every quarter, all of my hashed user IDs changed at once. From Anthropic's perspective, the entire user base of the service "vanished" and was replaced by a brand-new set of identities, which is exactly the opposite of the stable signal the field is meant to provide.
What I do now is keep the ANTHROPIC_USER_ID_SALT outside of my regular secret-rotation rotation, and document it explicitly in the runbook as a long-lived value. If a leak ever forces a rotation, I plan to coordinate it as a one-off event, with the explicit acceptance that any open conversation Anthropic was tracking against an old hashed ID will be cut. That trade-off feels right to me — short-term loss of continuity in exchange for closing the leak — but it is worth thinking about ahead of time rather than improvising under pressure.
A related practical detail: I write the hashed value to my own application logs as well, alongside the internal user ID. If you only keep the mapping in memory or compute it on the fly, you may struggle to answer "which of my users does this hash correspond to?" months later when the relevant request flow has changed. A flat log line per request, with both the internal ID and the hashed pseudo-ID, has saved me debugging time more than once.
What to try next
Pick one user identifier in your service, drop the to_anthropic_user_id helper above into your codebase, and start sending metadata.user_id on Messages API requests. Keep a mapping from internal IDs to hashed values in your own logs so that any future inquiry from Anthropic can be reconciled to a real account in seconds.
That single change quietly raises the defensive baseline for the whole service. Thanks for reading.