Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 9 min read Last reviewed Jul 2026

Microsoft Teams Error Guide: 'Request Entity Too Large' — Fix the 28KB Adaptive Card Limit

Quick answer

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
Free toolkit

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 RequestEntityTooLarge or 400 BadRequest when posting a card via bot, Graph, or webhook.
  • Cards with large tables, many Container/ColumnSet rows, or base64-inlined images fail while small cards from the same code path succeed.
  • The card shows as a blank message, truncated, or the fallbackText only.
  • 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, long ColumnSet tables, 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 the Image.url explodes the payload; a single screenshot can blow the whole budget.
  • Verbose data binding — sending a fully expanded card plus a large $data payload for templating, or repeating identical structures instead of using $data arrays.
  • 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/refresh or deep-link to a full report instead of rendering an entire result set.
  • Send template + data separately — use Adaptive Card templating with a compact $data array 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/jq check 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.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.