Observability & Control

Rate Limiting and Cost Control, Building It Yourself

⏱ 14 min

Neither the MCP Adapter nor the PHP AI Client SDK has a rate limiter or a budget system. There is no setting anywhere that caps how many times an agent can call an ability per minute, or how much a given integration is allowed to spend on model calls per day. The PHP AI Client SDK does expose a filter, wp_ai_client_prevent_prompt, that can block an individual prompt conditionally, but it is explicitly a single gate, not a quota tracker. This lesson builds the actual counting and enforcement yourself, using WordPress transients, and wires the result into both a permission_callback and that filter.

What you'll learn in this lesson
Why rate limiting is not a shipped feature here
What wp_ai_client_prevent_prompt actually is, and is not.
A transient-based counter as a practical rate limit
Bucketed by user and time window, checked before an ability runs.
Wiring the counter into permission_callback
So an over-limit agent is denied before execute_callback ever runs.
A daily cost-control counter using wp_ai_client_prevent_prompt
Blocking further model calls once a budget threshold is hit.
Prerequisites

Lesson 5 (permission-gated tools), and a basic idea of how WordPress transients work (set_transient / get_transient with an expiry).

Step 1: What actually exists, and what does not

No rate limiting in the MCP Adapter
No per-minute or per-hour call cap exists anywhere in the adapter itself.
No budget or quota system in the PHP AI Client SDK
There is no built-in way to say "stop calling the model once we have spent $50 today."
wp_ai_client_prevent_prompt is a single gate, not a tracker
It is a filter you can hook to block one specific prompt from proceeding, based on whatever condition you supply. It counts nothing on its own.
An unbounded agent connection is a real cost and abuse risk

Without any limiting, a misconfigured workflow, a runaway automation loop, or a compromised credential can call an ability (or trigger model calls through the AI Client SDK) as fast as the connection allows. For anything production-facing, treat rate limiting as a required control, not an optional extra, precisely because nothing enforces it for you by default.

Step 2: A transient-based counter

WordPress transients are a simple, already-available key-value store with a built-in expiry, exactly what’s needed for a windowed counter. This implementation buckets by user ID and a fixed time window:

// File: my-plugin.php
function my_plugin_check_and_increment_rate_limit( int $user_id, string $bucket_name, int $limit, int $window_seconds ): bool {
	$window_key = floor( time() / $window_seconds );
	$transient_key = "my_plugin_rl_{$bucket_name}_{$user_id}_{$window_key}";

	$count = (int) get_transient( $transient_key );

	if ( $count >= $limit ) {
		return false;
	}

	set_transient( $transient_key, $count + 1, $window_seconds );
	return true;
}
A windowed counter, not a strict sliding log

This is a fixed time-bucket counter: it resets cleanly at each window boundary rather than tracking a continuously sliding log of individual timestamps. That’s a reasonable, practical approximation for most agent rate limiting, and far simpler to implement correctly than a true sliding-window log. If you need stricter, smooth enforcement right at a window boundary, you’d store individual call timestamps instead and count how many fall within the trailing window, at the cost of more storage and complexity.

Step 3: Wire it into permission_callback

Check the limit before any execution happens, combined with the role and ownership checks from earlier lessons, not instead of them:

// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'my-plugin/summarize-recent-comments',
		array(
			// ...label, input_schema...
			'permission_callback' => function () {
				if ( ! current_user_can( 'my_plugin_summarize_comments' ) ) {
					return false;
				}

				$allowed = my_plugin_check_and_increment_rate_limit(
					get_current_user_id(),
					'summarize_comments',
					20,   // 20 calls
					60    // per 60-second window
				);

				if ( ! $allowed ) {
					return new WP_Error(
						'rate_limited',
						'This agent has exceeded its call limit for this action. Try again shortly.',
						array( 'status' => 429 )
					);
				}

				return true;
			},
			'execute_callback' => function ( $input ) {
				// summarization logic
			},
		)
	);
} );

Returning a WP_Error with a 429 status gives a well-behaved MCP client a clear, standard signal to back off, rather than a generic denial indistinguishable from a permissions failure.

Step 4: Daily cost control using wp_ai_client_prevent_prompt

For workflows where the ability itself triggers a further model call through the PHP AI Client SDK, wire the same counting pattern into the SDK’s own prevention filter, keyed by day instead of minutes, to approximate a daily budget:

// File: my-plugin.php
add_filter( 'wp_ai_client_prevent_prompt', function ( $should_prevent, $request ) {
	$today_key = 'my_plugin_ai_cost_' . gmdate( 'Y-m-d' );
	$count     = (int) get_transient( $today_key );

	$daily_limit = 200; // arbitrary example ceiling

	if ( $count >= $daily_limit ) {
		return true; // prevent this prompt from proceeding
	}

	set_transient( $today_key, $count + 1, DAY_IN_SECONDS );
	return $should_prevent;
}, 10, 2 );
This counts calls, not tokens or dollars

A per-call counter is a reasonable proxy for cost control but is not the same as tracking actual token usage or billed amount. If your model provider’s response includes token or usage data, capture and sum that instead of a flat call count for a more accurate budget signal, the transient storage pattern here works the same either way.

Test it: confirm the limit actually trips

Call the rate-limited ability more than its configured limit in quick succession from your AI client, or directly:

wp-env run cli wp eval '
for ( $i = 0; $i < 25; $i++ ) {
	$ability = wp_get_ability( "my-plugin/summarize-recent-comments" );
	$result  = $ability->execute( array() );
	echo $i . ": " . ( is_wp_error( $result ) ? $result->get_error_code() : "ok" ) . "\n";
}
'

You should see ok for the first 20 calls and rate_limited afterward, resetting once the 60-second window passes.

Recap

Rate limiting and cost control are not features the MCP Adapter or the PHP AI Client SDK ship. wp_ai_client_prevent_prompt is a real, usable enforcement point, but it only blocks a prompt when you tell it to, it tracks nothing by itself. A transient-based counter, bucketed by user and a fixed time window, is a practical, easy-to-reason-about way to build the actual limiting logic, wired into permission_callback for ability calls and into wp_ai_client_prevent_prompt for direct model usage. This closes out the course: authentication, permissions, data protection, and now control over volume and cost, all built from real, verifiable mechanisms rather than assumed ones.

Resources & further reading

← Managing and Revoking Agent Access Finish ✓