Microsoft Teams Error Guide: 'Request Entity Too Large' — Fix the 28KB Adaptive Card Limit
Fix Microsoft Teams 'Request Entity Too Large' and dropped Adaptive Cards caused by the ~28KB payload limit: trim card JSON, paginate results, offload images, and template instead of inlining.
- #microsoft-teams
- #adaptive-cards
- #troubleshooting
- #errors
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.
Overview
Teams silently drops or rejects Adaptive Cards whose serialized payload exceeds the platform limit (roughly 28KB for a single card/activity). Depending on the delivery path you see one of these. Bot Framework / Graph chatMessage posts return an HTTP 413:
HTTP/1.1 413 Request Entity Too Large
{
"error": {
"code": "RequestEntityTooLarge",
"message": "The request message is too large. The maximum message size is 28 KB."
}
}
An Incoming Webhook / Workflows post can return a 400 instead:
HTTP/1.1 400 Bad Request
{ "error": { "code": "BadRequest", "message": "Card size exceeds the maximum allowed size." } }
And the most confusing case: the send returns 200/201, but the card renders as an empty bubble or a plain-text fallback because the client refused to render an oversized body.
Symptoms
413 RequestEntityTooLargeor400 BadRequestwhen posting a card via bot, Graph, or webhook.- Cards with large tables, many
Container/ColumnSetrows, or base64-inlined images fail while small cards from the same code path succeed. - The card shows as a blank message, truncated, or the
fallbackTextonly. - Failures appear only for the “big” incidents — a deploy digest with 200 rows fails while a 5-row one works.
- Localized cards fail after translation inflates the string length.
Common Root Causes
- Too many elements — large
FactSets, longColumnSettables, or one card trying to show an entire result set instead of a page. - Inlined base64 images — embedding a PNG/screenshot as a
data:URI in theImage.urlexplodes the payload; a single screenshot can blow the whole budget. - Verbose data binding — sending a fully expanded card plus a large
$datapayload for templating, or repeating identical structures instead of using$dataarrays. - Deeply nested containers — redundant wrappers and repeated styling on every element add up.
- Whitespace / duplicated JSON — pretty-printed payloads and duplicated attachments in a single activity.
- Localization inflation — translated strings (German, Finnish) are longer and can push a borderline card over the edge.
Diagnostic Workflow
First, measure the real serialized size of the exact JSON you send — not the template, the final payload. This is the single most useful check:
# Bytes of the serialized card body (compact, as Teams counts it)
jq -c . card.json | wc -c
If that number approaches 28672 (28 * 1024), you are at the limit. For a bot/Graph attachment, the whole activity counts, so measure the wrapped body:
jq -c '{type:"message", attachments:[{contentType:"application/vnd.microsoft.card.adaptive", content: .}]}' card.json | wc -c
Reproduce the rejection against your delivery path. For a bot connector / Graph send:
curl -i -X POST "https://graph.microsoft.com/v1.0/teams/TEAM_ID/channels/CHANNEL_ID/messages" \
-H "Authorization: Bearer $GRAPH_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @activity.json
Expect the 413 body above when oversized. For an Incoming Webhook / Workflows URL:
curl -i -X POST "$TEAMS_WEBHOOK_URL" \
-H "Content-Type: application/json" \
--data-binary @webhook-payload.json
Find what is eating the budget. List the largest fields so you can target the offender (usually an inlined image):
# Show byte size of each top-level image url / large string
jq -c '.. | select(type=="string") | {len: length, head: .[0:40]}' card.json \
| jq -s 'sort_by(-.len) | .[0:10]'
If a data:image/png;base64,... string dominates, that is your root cause. Confirm the element count is sane:
jq '[.. | objects | select(has("type"))] | length' card.json # total element count
Example Root Cause Analysis
An SRE team’s deploy-digest bot posted an Adaptive Card summarizing every changed service after a release train. Small releases posted fine; the quarterly big-bang release returned 413 RequestEntityTooLarge and on-call got no card at all.
jq -c . card.json | wc -c reported 41,900 bytes. The jq size breakdown showed two things: a FactSet with 180 rows (one per service) and an inlined base64 screenshot of the pipeline graph worth ~14KB on its own.
Two fixes brought it under budget. First, the screenshot moved out of the card — it was uploaded to blob storage and referenced by a short HTTPS Image.url instead of a data: URI, removing ~14KB. Second, the 180-row FactSet was replaced with a summary card (counts by status) plus an Action.OpenUrl deep-linking to a full report, and, where the detail was needed in-channel, paginated into cards of 25 rows using an Action.Execute “Show more” with refresh. The final card measured 6.8KB and rendered instantly. The team added a CI assertion (wc -c < 24000) so no future template can silently cross the limit.
Prevention Best Practices
- Never inline images as base64 — host the image and reference it by HTTPS URL; this alone reclaims the most space.
- Paginate, don’t dump — show a summary plus a “Show more”
Action.Execute/refreshor deep-link to a full report instead of rendering an entire result set. - Send template + data separately — use Adaptive Card templating with a compact
$dataarray rather than a fully expanded, repeated structure. - Budget with headroom — target roughly 24KB so localization or an extra row cannot push you over 28KB.
- Assert size in CI — add a
wc -c/jqcheck that fails the build if the serialized card exceeds your budget. - Flatten nesting — remove redundant containers and set shared styling at the container level rather than per element.
- Always set
fallbackText— so an over-limit or unsupported card degrades to readable text instead of an empty bubble.
Quick Command Reference
# Measure serialized card size (compare to 28672 bytes)
jq -c . card.json | wc -c
# Measure the full activity/attachment size (what a bot/Graph send counts)
jq -c '{type:"message",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:.}]}' card.json | wc -c
# Rank the largest strings (find inlined images / bloated fields)
jq -c '.. | select(type=="string") | {len:length, head:.[0:40]}' card.json | jq -s 'sort_by(-.len)|.[0:10]'
# Count total elements
jq '[.. | objects | select(has("type"))] | length' card.json
# Reproduce the send against a webhook and read the status
curl -i -X POST "$TEAMS_WEBHOOK_URL" -H "Content-Type: application/json" --data-binary @webhook-payload.json
Conclusion
Teams’ ~28KB Adaptive Card limit turns “more detail” into dropped alerts. The fix is almost always the same two moves: stop inlining images (reference them by URL) and stop dumping full result sets (summarize, paginate, or deep-link). Measure the real serialized payload with jq -c . | wc -c, budget to ~24KB for localization headroom, and add a CI size assertion so a growing template can never silently push a card over the edge and leave on-call staring at an empty message.
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.