# Rate-limit OAuth /register for AI clients, not human OAuth

Web page: https://academy.memberpress.ai/courses/agent-discoverable-surfaces/l1/

Lesson 4 of 7 in [Agent-Discoverable Surfaces](https://academy.memberpress.ai/courses/agent-discoverable-surfaces/), 3 min.

Video: https://www.youtube.com/watch?v=7NO7X7xx9xA

**Hook**

The server logs say "rate limit working as designed." The user's connector UI says "couldn't connect." Nothing in between names the cause. The rate limiter on your `/register` endpoint was tuned for one developer testing one app — not for thousands of users sharing one AI client's egress IP.

**Concept**

Prerequisites: you have shipped a public HTTP endpoint, you know what OAuth dynamic client registration (RFC 7591) is, you have read the agent-discovery-loop lesson in `api-fundamentals`, and you have written a naive per-IP rate limiter. This lesson teaches how to *recalibrate* one for a traffic shape you have not seen yet. `/register` here is the OAuth dynamic-client-registration endpoint, where a new client introduces itself *before* it has credentials. Not your authenticated API.

**Why human OAuth and AI OAuth look the same on paper but break in opposite directions**

An apartment building has one front door. Human OAuth is the resident model: you move in, get a key, and never queue at the door again. The lock can be slow, the lobby small — none of it matters. The resident passes through once and reuses the key for years.

AI OAuth is the visitor model. The AI client *does not keep keys.* Claude.ai re-registers an OAuth client on every connection attempt — no `client_id` persists across user sessions. Claude.ai also routes a whole region through a small set of egress IPs, so every visitor knocks on the same door, from the same address, every time they want in. The door is the lobby is the queue. A 5-per-hour-per-IP limit sized for one resident-per-year evaporates by lunch.

**Walk-through**

The tutorial default you copied has two flaws: a 5/hour threshold (a residential lobby, not a public turnstile) and a 429 with no `Retry-After` and no actionable message. The AI client retry-storms; the user sees a generic failure. The fix raises the threshold and makes the 429 carry useful information:

```php
// register-controller.php (after)
private function check_registration_rate_limit(): ?WP_REST_Response
{
    $ip       = $this->get_client_ip();
    $key      = 'mp_mcp_register_' . md5($ip);
    $attempts = (int) get_transient($key);

    if ($attempts >= 60) {
        return new WP_REST_Response(
            [
                'error'             => 'too_many_requests',
                'error_description' => 'Too many client registrations from this IP. Please try again in an hour.',
            ],
            429,
            ['Retry-After' => '3600']
        );
    }

    set_transient($key, $attempts + 1, HOUR_IN_SECONDS);
    return null;
}
```

The Flask shape is the same three moves:

```python
# routes/register.py
if attempts >= 60:
    return jsonify(
        error="too_many_requests",
        error_description="Too many client registrations from this IP. Please try again in an hour.",
    ), 429, {"Retry-After": "3600"}
```

60/hour is roughly one per minute — still abuse-resistant (an attacker can't fingerprint your tenant boundary or exhaust resources at that rate), but it absorbs dozens of legitimate AI users behind one egress IP. As of 2026-04-30 this is the working number for Claude.ai connector traffic. Treat it as a current example, not a ceiling. Watch your 429 rate after deploy and raise the floor if real users still hit it.

Reproduce the failure locally before users do:

```bash
# scripts/probe-register.sh
for i in {1..70}; do
  curl -s -o /dev/null -w "%{http_code}\n" \
    -X POST https://yoursite.com/oauth/register \
    -H "Content-Type: application/json" \
    -d '{"client_name":"probe","redirect_uris":["http://localhost"]}'
done
# → 200 for the first batch, then 429 once you cross the threshold
```

If the 429 arrives without a `Retry-After` or with a generic "Try again later" body, the threshold is fine but the diagnostic is invisible — the silent-failure shape survived the bump.

Field signature: when a Claude Desktop user reports "couldn't connect," ask for the connector URL and look for `step=start_error`. That fragment means `/register` failed before login could start. The probe above reproduces the same shape from your laptop.

**If you get stuck**

- **The limit lives in a layer you do not control (CDN, WAF, hosting plan).** Your application code looks correct; the 429 is emitted upstream with no `Retry-After`. Find the layer, move the limit into your app, or have the upstream provider widen it for `/register`.
- **You raised the threshold but the 429 still has no `Retry-After`.** AI clients without that header retry on their own schedule, which compounds the load. Add the header. One line.
- **Your error message is generic, so the user can't self-diagnose.** "Try again later" tells them nothing. Replace with a specific window — "Try again in an hour" — and AI client surfaces pass it through to the user where it does some good.

**Cross-reference**

The agent-discovery-loop lesson in `api-fundamentals` (l9) is the public-MCP plus `.well-known/mcp.json` pattern this auth surface lives behind. Every endpoint in that discovery file shares this calibration problem: any per-IP limit on a path AI clients touch during connection setup needs to be sized for shared egress, not single-developer traffic.

**Deliverable**

Your `/register` endpoint accepts at least 60 registrations per IP per hour. On overflow it returns `429`, a `Retry-After: 3600` header, and an `error_description` naming the wait time in concrete units. You can describe — without checking notes — the diagnostic fragment in a Claude Desktop connector URL that tells you a `/register` rate limit fired before login could start.

**Further reading**

- ["Closing the Agent Discovery Loop"](https://sethshoultes.com/blog/closing-the-agent-discovery-loop.html) — the conceptual framing for why an AI agent's traffic shape differs from a human's, and what the public surface needs to look like.

## Try these prompts

- Rewrite a check_registration_rate_limit() function so the threshold is 60/hour and the 429 response includes a Retry-After header and an actionable error_description.
- Describe the diagnostic signature in Claude Desktop that tells you a /register rate limit fired before login could even start, and the manual POST that reproduces it.
- List three endpoints in an MCP or OAuth flow that AI clients hit during connection setup and explain why per-IP rate limits there need calibration for shared egress, not human single-user traffic.

## Transcript

In this lesson, you'll learn why a rate limit that's perfect for human sign-ins can lock out AI clients, and how to recalibrate it. Here's the situation. Your server logs say the rate limit is working as designed. Your user sees, couldn't connect.

The endpoint involved is the register endpoint in OAuth, where a new app introduces itself before it has any credentials. Tutorial code often limits it to five registrations an hour from each IP address. Here's why that breaks. Think of an apartment building with one front door.

Human sign-in works like a resident. You move in, you get a key, and you never queue at the door again. AI clients work like visitors. Claude dot AI registers a new client every time it connects, and keeps no key between sessions.

It also sends a whole region's traffic out through a small set of IP addresses. So every visitor knocks on the same door, from the same address, every time. A limit of five an hour is gone by lunch. So here's the fix, and it has two parts.

First, raise the limit to sixty an hour per IP address, about one a minute. That's still too slow for an attacker to do harm, but it lets in dozens of real users sharing one address. It's the working number as of this spring, so treat it as an example, not a ceiling. Watch how often the limit fires after you deploy, and raise it if real people still hit it.

Second, make the refusal useful. When you send back the too many requests error, add a retry after header set to one hour, and a plain message that says, try again in an hour. Without that header, AI clients retry on their own schedule. Reproduce it yourself before your users do.

The lesson page has a short script that registers seventy times in a row. You should see successes, then refusals once you cross the line. If a refusal comes back with no retry header, or just says try again later, you've fixed the number but left the problem invisible. Here's how to spot it in the wild.

When a Claude Desktop user says they couldn't connect, ask for the connector address and look for step equals start error. That means registration failed before sign-in even began. The common mistake is fixing your code when the limit lives somewhere else, like your content delivery network, a firewall, or your hosting plan. If your code looks right but the refusals still have no retry header, find the layer that's sending them, and either move the limit into your app or ask your provider to widen it.

So remember. Size your limits for shared traffic, and make every refusal say exactly how long to wait.

Next: [Two-axis MCP authorization — scope and capability](https://academy.memberpress.ai/courses/agent-discoverable-surfaces/l2/)
