Microsoft Teams Error: 'Operation returned an invalid status code 'TooManyRequests'' — Cause, Fix, and Troubleshooting Guide
Fix the Bot Service SDK 'Operation returned an invalid status code TooManyRequests' (HTTP 429): honor Retry-After, throttle proactive sends, and back off.
- #microsoft-teams
- #troubleshooting
- #errors
- #bot-framework
Stuck on this Microsoft Teams error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
What this error means
When the Bot Framework connector responds with HTTP 429 Too Many Requests, the Azure Bot Service SDK surfaces it as an ErrorResponseException whose message is Operation returned an invalid status code 'TooManyRequests'. The connector throttles because your bot is exceeding the per-bot and per-conversation send limits Teams enforces on the messaging connector.
The exception message from the .NET SDK (Java is analogous):
Microsoft.Bot.Schema.ErrorResponseException:
Operation returned an invalid status code 'TooManyRequests'
The underlying HTTP response carries the retry hint:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
This is distinct from Microsoft Graph throttling. Graph 429s come from graph.microsoft.com and are covered in the 429 Too Many Requests guide. Here the throttle is on the Bot Framework connector (serviceUrl), reached through the SDK, and the SDK wraps the status in an ErrorResponseException rather than handing you the raw HTTP response.
Warning signs
- Sends throw
ErrorResponseException: Operation returned an invalid status code 'TooManyRequests'. - The wrapped
Response.StatusCodeis429and aRetry-Afterheader is present. - Bursts of proactive messages start failing partway through a batch.
- A high-fanout notification (one message to many conversations) throttles as the loop progresses.
- Rapid replies within a single busy conversation begin to fail.
- Failures cluster during scheduled broadcasts and subside when send rate drops.
Measuring the limit
Catch the ErrorResponseException, read the wrapped status code, and extract the Retry-After value. In C# with the Bot Framework SDK:
try
{
await turnContext.SendActivityAsync(activity, cancellationToken);
}
catch (ErrorResponseException ex) when (ex.Response?.StatusCode == HttpStatusCode.TooManyRequests)
{
var retryAfter = ex.Response.Headers.TryGetValues("Retry-After", out var vals)
? vals.FirstOrDefault()
: null;
logger.LogWarning("Connector throttled. Retry-After={RetryAfter}s", retryAfter);
// schedule a retry after the indicated delay
}
ex.Body?.Error?.Code and ex.Body?.Error?.Message give you the structured connector error when present. In Node.js, inspect the error’s statusCode and the response headers on the throttled turn:
onTurnError = async (context, error) => {
if (error.statusCode === 429) {
console.warn("Connector 429; honor retry-after before resending");
}
};
If you are reproducing against the connector directly, the raw response confirms the throttle:
curl -si -X POST \
"${serviceUrl}v3/conversations/${conversationId}/activities" \
-H "Authorization: Bearer $BOT_TOKEN" \
-H "Content-Type: application/json" \
-d @activity.json | grep -iE '^(HTTP|retry-after)'
Limits and pressure causes
- Unthrottled proactive fan-out. Looping over hundreds of conversations and sending as fast as the code runs exceeds the per-bot send rate.
- No Retry-After handling. Catching the exception and immediately resending, or retrying with a fixed short delay, keeps you over the limit.
- Per-conversation hotspots. Posting many activities to one conversation in a tight window trips the per-conversation ceiling even when overall volume is modest.
- Parallel workers sharing one bot identity. Multiple instances/threads sending concurrently sum against the same per-bot budget.
- Chatty typing/update patterns. Sending frequent typing indicators or activity updates alongside real messages inflates request count.
- Retry storms. Treating every transient failure as an immediate retry multiplies request volume precisely when the connector is already throttling.
Remediation
Honor Retry-After. On a 429, wait exactly the number of seconds the header specifies before resending that activity — do not resend immediately and do not use a shorter delay. When the header is absent, fall back to exponential backoff with jitter.
Throttle proactive sends at the source. Push outbound activities through a queue with a rate limiter so you send at a steady pace below the connector’s limits rather than bursting:
// e.g. a SemaphoreSlim + delay, or a channel/queue consumed by a paced worker
await rateLimiter.WaitAsync(); // caps sustained send rate
await SendWithRetryAsync(reference, activity); // retries on 429 using Retry-After
For large fan-outs, spread the batch over time and add a small delay between conversations; do not parallelize the same bot identity beyond what the per-bot limit allows. Space out repeated posts to a single conversation to respect the per-conversation ceiling. Make retries idempotent and bounded so a throttling episode does not turn into a retry storm.
Capacity headroom
- Retry-After is authoritative. Wait the full indicated interval; guessing a shorter delay extends the throttle window.
- Two different 429 sources. Connector
429(this error, via the SDK exception) is separate from Graph429; fix them in the right layer. - Per-bot and per-conversation limits both exist. Low overall volume can still trip the per-conversation limit in a hot thread.
- The SDK hides the raw response. You must inspect
ErrorResponseException.Responseto get the status and headers. - Parallelism shares the budget. Scaling out instances of the same bot does not multiply your send allowance.
- Backoff needs jitter. Synchronized retries across workers re-collide; randomize the delay.
Related capacity errors
Fixed it? Get 500 Microsoft Teams & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.