Limits and quotas
Most surprising SZ-MCP behaviour is a limit doing its job. This page collects them, with what each one does when you reach it.
Timeouts
Section titled “Timeouts”| Limit | Value | On reaching it |
|---|---|---|
code_mode run budget | 30 seconds wall clock, shared across every call the program makes | From 20 seconds, every call result carries a budget.warning telling the program to stop looping and return. At 30 seconds the run ends with a tool error: { "error": "timeout", "message": "code_mode exceeded its 30 s wall-clock limit. …", "writesSent": [...] }. Requests already sent are not cancelled; writesSent lists any writes |
One call request to SmartZone | 15 seconds | The request is aborted; surfaces as a network_error envelope, and the time still counts against the run budget |
| Service ticket mint (login) | 15 seconds | invalid_host — the controller did not respond in time |
| API version detection | 15 seconds per request | Detection reads the controller’s unauthenticated apiInfo list, picks the newest version and logs in once. A timeout fails with invalid_host; a host that does not answer as a SmartZone gets not_smartzone and never receives the password |
await_approval | 15 seconds | Returns { status: 'pending' }; call it again or re-send with the approvalId later |
| Confirm token, dashboard approval | 10 minutes | The write is previewed again with a fresh token (token_expired) or a new approval |
The run budget is per code_mode call, not per conversation. Every new call
gets a fresh 30 seconds, which is why the right response to hitting it is to
split the work rather than retry the same program.
| Limit | Value | On exceeding it |
|---|---|---|
code_mode program source | 20,000 characters | Rejected before execution: code must be under 20,000 characters |
code_mode result | 200 KB | Returned as { truncated: true, originalChars, hint, preview } with the first 100 KB |
| One controller response body | 32 MB | body becomes { error: 'response_too_large', message, preview } with the first 2,000 characters |
| MCP request body | 4 MB (OAuth endpoints 64 KB) | 413 with {"error": "payload_too_large"} |
| JSON-RPC batch | 10 requests | batch_too_large error |
| Controller port | 8443 or 443 only | forbidden_port |
| Endpoint note | 5–500 characters; 10 per endpoint, 500 per organisation | note_too_short, note_too_long, too_many_notes_for_endpoint, too_many_notes |
| Host / FQDN | 253 characters | Rejected on save |
| SmartZone username | 200 characters | Rejected on save |
| SmartZone password | 500 characters | Rejected on save |
| Credential label | 100 characters | Rejected on save |
Result caps
Section titled “Result caps”These bound what the discovery primitives return in one call. Neither is a cap on what exists — both report the full total separately.
| Primitive | Default | Maximum | Total available |
|---|---|---|---|
search_endpoints | 20 results | 50 | count gives the pre-limit total |
list_endpoints_by_tag | 50 results | 200 | count gives the total under the tag |
list_endpoints_by_tag takes an offset, so a tag larger than 200 operations
is paged rather than truncated. search_endpoints has no offset — narrow the
query or add a tag instead.
get_endpoint_details inlines $ref pointers to a depth of 3. Deeper
references, and any cycle, are left as { $ref: "…" } rather than expanded.
Caches and lifetimes
Section titled “Caches and lifetimes”| Item | Lifetime | Notes |
|---|---|---|
| Service ticket | 25 minutes | Cached in KV per organisation, deliberately shorter than SmartZone’s idle timeout. Flushed on credentials save, delete and Test connection |
| Full OpenAPI spec | Per isolate, until eviction | Loaded from object storage on the first get_endpoint_details for that surface that misses the cache below; adds a few hundred ms to that one call |
| One endpoint’s details | 30 days in KV, plus the last 200 per isolate | Invalidated when a new index is deployed. caveats and notes are added on every read, outside the cache |
| Web session cookie | 30 days | szmcp_session |
| MCP access token | 30 days | Prefixed szmk_ |
| MCP refresh token | 90 days | Rotated on every use; the old one is revoked |
| OAuth authorization code | 10 minutes | Single use |
| MCP client registration | 30 days | Claude re-registers automatically |
Retries
Section titled “Retries”call never throws on an HTTP error, and retries exactly one thing:
| Status | Behaviour |
|---|---|
| 401 | The cached ticket is discarded, a fresh one is minted, and the request is retried once. A second 401 is returned to you |
| 429 | Retried up to 2 times for GET, PUT and DELETE, and for POST/PATCH only with retryOn429: true. Each retry waits for Retry-After, capped at 5 seconds (1 second when absent). retried counts them; a third 429 is returned to you |
| Everything else | Returned as-is. No automatic retry on 5xx or any other status |
Those waits count against the run budget. A controller that keeps answering 429, or one that is flapping, surfaces directly to the model.
Rate limits
Section titled “Rate limits”| Limit | Value | On reaching it |
|---|---|---|
| MCP client registrations | 20 per IP per hour | 429 with {"error": "rate_limited"} |
| Authenticator codes | 5 per 5 minutes per person (chat and dashboard together) | totp_locked; the confirm token stays valid until it expires |
| Audit evidence pack | 6 per minute per organisation | 429 |
| Alert test sends | 5 per hour per organisation | 429 |
| Configuration bundle export and import (dashboard) | 10 per minute per organisation | 429 |
| Controller tunnel creation | 5 per hour per organisation | 429 |
The registration limit bounds the Dynamic Client Registration endpoint only.
Normal use registers once per connector, so reaching it usually means something
is re-registering in a loop. code_mode calls themselves are not rate limited;
the run budget bounds them.
Change management caps
Section titled “Change management caps”| Limit | Value | On reaching it |
|---|---|---|
| MOPs per organisation | 100 | mop.save refuses a new one |
| Steps per MOP | 200 | mop.save refuses the MOP |
Items one forEach step repeats over | 200 | The step fails without sending, and the run fails |
| Runs kept | 20 per MOP, 90 days | Older finished runs are deleted, oldest first |
| An approved run’s writes | Sent for 12 hours after the run starts | arming_expired: abort the run and start a new one. Past 12 hours a run can no longer be rolled back as a whole; undo its writes one by one with history.undo |
| Change window | Up to 24 hours (default 60 minutes) | mop.save refuses the MOP |
| A step with no result | Lost after 45 seconds | The run waits for you to retry or skip it |
| Active MOP schedules | 20 per organisation | mop.schedule refuses another |
A once schedule | At most 30 days ahead | Refused |
| A recurring schedule’s approval | 30 days by default, 90 days at most | Refused above 90 days; nothing fires after until |
| A scheduled run found late | 10 minutes | Recorded as missed, never run late |
| Lint fixes in one MOP | 50 | Pick fewer findings |
| Post-change verification | Watches 15 minutes; at most 50 at once | A write past 50 is not verified, and the result says so |
switches.restore | Needs a backup of that switch from the last 15 minutes | fresh_backup_required unless force |
Retention
Section titled “Retention”| Data | Kept | Then |
|---|---|---|
| Write history (body, before-state, response) | 30 days, at most 5,000 writes | Deleted; those writes can no longer be undone. Fields over 64 KB (body), 256 KB (before-state) or 16 KB (response) are dropped when stored, and such a write cannot be undone |
| Write log (who, what, result, body hash) | 365 days | Deleted |
| Post-change verifications | 30 days, at most 5,000 | Deleted |
| Config snapshot changes | 180 days, at most 50,000 | Deleted, oldest first |
| Controller admin audit log | 400 days, at most 100,000 rows | Deleted, oldest first |
| Streamed events, alarms, client sessions and connection attempts | 14 days (an admin can set 1–14) | Deleted |
| Streamed application flows | 2 days (an admin can set 0–2; 0 stores none) | Deleted |
| Metrics | Raw 48 hours, hourly 8 days, daily 400 days | Queries further back run on the coarser tier |
| Alert history | 30 days; HARD transitions, recoveries, acknowledgements, downtime and rule changes 400 days | Deleted |
| Site notes | 5,000 per organisation, 50 per device or location, 2,000 characters each | Refused past the cap; an expired note is deleted 30 days after it expires |
Account scope
Section titled “Account scope”| Limit | Value |
|---|---|
| SmartZone controllers per organisation | One |
| MCP scopes | One — sz:call |
| MCP tools exposed | One — code_mode |
Saving new credentials replaces the organisation’s controller. A person can belong to several organisations, each with its own controller, and each connector is bound to the organisation chosen when it was authorised.
Limits that are not SZ-MCP’s
Section titled “Limits that are not SZ-MCP’s”Two ceilings sit outside this service and are worth naming, because they look like SZ-MCP limits from the inside:
- Your SmartZone account’s permissions. Anything it cannot do returns a
403from the controller. SZ-MCP cannot widen them; what it adds on top is the write guard, your role in the organisation and read-only mode — see Security model. - The controller’s own pagination. WSG list endpoints typically take
listSizeandindexand return{ list, hasMore, totalCount }. Those caps are SmartZone’s; SZ-MCP passes your query through unchanged.