Discord Embed Limits Cheat Sheet (Characters, Fields & Total Payload)
Every Discord embed limit in one place: title, description, fields, footer, total payload size. With validation code in Python and JavaScript to catch overflow before sending.
Discord rejects oversized embeds with a generic 400 Bad Request. The error message is rarely helpful — it just says something is too long, not which field. This guide is the complete cheat sheet of every embed limit Discord enforces in 2026, plus drop-in validators for Python and JavaScript that report exactly which limit you blew before you hit Discord’s API.
The Numbers (2026)
Per-Embed Limits
| Field | Max | Counts toward total? |
|---|---|---|
title | 256 chars | yes |
description | 4096 chars | yes |
fields | 25 entries | — |
field.name | 256 chars | yes |
field.value | 1024 chars | yes |
footer.text | 2048 chars | yes |
author.name | 256 chars | yes |
url, image.url, thumbnail.url, author.url, footer.icon_url, author.icon_url | 2048 chars | no |
color | 24-bit integer (0–16777215) | — |
timestamp | ISO-8601 | — |
Per-Message Limits
| Field | Max |
|---|---|
content | 2000 chars (4000 with Nitro) |
embeds | 10 per message |
Total embed text (sum of all title + description + field.name + field.value + footer.text + author.name across all embeds) | 6000 chars |
attachments | 10 files |
| File size | 25 MB free / 50–500 MB tier-dependent |
Webhook-Specific
| Field | Max |
|---|---|
username | 80 chars (and cannot be “Clyde” or “Discord”) |
avatar_url | 2048 chars |
thread_name | 100 chars (when starting a forum thread) |
The Trap: 6000-Char Total
You can technically have 10 embeds × 4096 char descriptions = 40,960 chars… but Discord rejects anything over 6,000 chars summed across all embeds in the message. Hit that and you get 400 even though no individual field exceeded its limit.
This catches people building dashboards that pile up multiple embeds — at scale, you blow the total long before any one embed.
Python Validator
from typing import Any
class EmbedLimitError(ValueError):
pass
# Per-field limits, in chars
LIMITS = {
"title": 256,
"description": 4096,
"field.name": 256,
"field.value": 1024,
"footer.text": 2048,
"author.name": 256,
"username": 80,
"content": 2000,
}
TOTAL_EMBED_CHARS = 6000
MAX_FIELDS = 25
MAX_EMBEDS = 10
def _len(s: Any) -> int:
return len(s) if isinstance(s, str) else 0
def validate_embed(e: dict, idx: int = 0) -> int:
"""Validate one embed; return its char total."""
total = 0
if _len(e.get("title")) > LIMITS["title"]:
raise EmbedLimitError(f"embeds[{idx}].title > {LIMITS['title']}")
total += _len(e.get("title"))
if _len(e.get("description")) > LIMITS["description"]:
raise EmbedLimitError(f"embeds[{idx}].description > {LIMITS['description']}")
total += _len(e.get("description"))
fields = e.get("fields", [])
if len(fields) > MAX_FIELDS:
raise EmbedLimitError(f"embeds[{idx}].fields > {MAX_FIELDS} entries")
for fi, f in enumerate(fields):
if _len(f.get("name")) > LIMITS["field.name"]:
raise EmbedLimitError(f"embeds[{idx}].fields[{fi}].name > {LIMITS['field.name']}")
if _len(f.get("value")) > LIMITS["field.value"]:
raise EmbedLimitError(f"embeds[{idx}].fields[{fi}].value > {LIMITS['field.value']}")
total += _len(f.get("name")) + _len(f.get("value"))
footer = e.get("footer", {})
if _len(footer.get("text")) > LIMITS["footer.text"]:
raise EmbedLimitError(f"embeds[{idx}].footer.text > {LIMITS['footer.text']}")
total += _len(footer.get("text"))
author = e.get("author", {})
if _len(author.get("name")) > LIMITS["author.name"]:
raise EmbedLimitError(f"embeds[{idx}].author.name > {LIMITS['author.name']}")
total += _len(author.get("name"))
return total
def validate_payload(payload: dict) -> None:
"""Raise EmbedLimitError if any limit is broken."""
if _len(payload.get("content")) > LIMITS["content"]:
raise EmbedLimitError(f"content > {LIMITS['content']}")
if _len(payload.get("username")) > LIMITS["username"]:
raise EmbedLimitError(f"username > {LIMITS['username']}")
embeds = payload.get("embeds", [])
if len(embeds) > MAX_EMBEDS:
raise EmbedLimitError(f"embeds > {MAX_EMBEDS}")
grand_total = sum(validate_embed(e, i) for i, e in enumerate(embeds))
if grand_total > TOTAL_EMBED_CHARS:
raise EmbedLimitError(f"total embed chars {grand_total} > {TOTAL_EMBED_CHARS}")
Use it before every requests.post:
import requests
payload = {"embeds": [{"title": "x" * 300}]} # over 256
try:
validate_payload(payload)
except EmbedLimitError as e:
print(f"Won't send: {e}")
else:
requests.post(WEBHOOK_URL, json=payload)
JavaScript / TypeScript Validator
interface Embed {
title?: string;
description?: string;
fields?: Array<{ name: string; value: string; inline?: boolean }>;
footer?: { text?: string };
author?: { name?: string };
}
interface Payload {
content?: string;
username?: string;
embeds?: Embed[];
}
const LIMITS = {
title: 256,
description: 4096,
fieldName: 256,
fieldValue: 1024,
footerText: 2048,
authorName: 256,
username: 80,
content: 2000,
};
const TOTAL_CHARS = 6000;
const MAX_FIELDS = 25;
const MAX_EMBEDS = 10;
const len = (s?: string) => (typeof s === 'string' ? s.length : 0);
function validateEmbed(e: Embed, idx = 0): number {
let total = 0;
if (len(e.title) > LIMITS.title) throw new Error(`embeds[${idx}].title > ${LIMITS.title}`);
total += len(e.title);
if (len(e.description) > LIMITS.description)
throw new Error(`embeds[${idx}].description > ${LIMITS.description}`);
total += len(e.description);
const fields = e.fields ?? [];
if (fields.length > MAX_FIELDS)
throw new Error(`embeds[${idx}].fields > ${MAX_FIELDS}`);
fields.forEach((f, fi) => {
if (len(f.name) > LIMITS.fieldName)
throw new Error(`embeds[${idx}].fields[${fi}].name > ${LIMITS.fieldName}`);
if (len(f.value) > LIMITS.fieldValue)
throw new Error(`embeds[${idx}].fields[${fi}].value > ${LIMITS.fieldValue}`);
total += len(f.name) + len(f.value);
});
if (len(e.footer?.text) > LIMITS.footerText)
throw new Error(`embeds[${idx}].footer.text > ${LIMITS.footerText}`);
total += len(e.footer?.text);
if (len(e.author?.name) > LIMITS.authorName)
throw new Error(`embeds[${idx}].author.name > ${LIMITS.authorName}`);
total += len(e.author?.name);
return total;
}
export function validatePayload(p: Payload): void {
if (len(p.content) > LIMITS.content) throw new Error(`content > ${LIMITS.content}`);
if (len(p.username) > LIMITS.username) throw new Error(`username > ${LIMITS.username}`);
const embeds = p.embeds ?? [];
if (embeds.length > MAX_EMBEDS) throw new Error(`embeds > ${MAX_EMBEDS}`);
const grand = embeds.reduce((sum, e, i) => sum + validateEmbed(e, i), 0);
if (grand > TOTAL_CHARS) throw new Error(`total embed chars ${grand} > ${TOTAL_CHARS}`);
}
Truncation Helper
When user input might overflow, truncate gracefully with an ellipsis:
def truncate(s: str, n: int) -> str:
return s if len(s) <= n else s[:n - 1] + "…"
embed = {
"title": truncate(user_title, 256),
"description": truncate(user_body, 4096),
"fields": [
{"name": truncate(k, 256), "value": truncate(v, 1024), "inline": True}
for k, v in metadata.items()
][:25], # cap field count too
}
How Discord Counts Characters
- Code points, not bytes. A Cyrillic
Яand an emoji🚀both count as 1 (well, emoji can be 1–4 code points but Discord measures the rendered glyph) - Markdown formatting (
**bold**) does count — the asterisks consume 4 chars - Custom emoji (
<:name:123>) count as the literal string length (~17 chars) - Mentions (
<@123>) count as the literal string
If your input is exactly 4096 chars but contains markdown, you’ll be over after Discord renders the asterisks — bake markdown overhead into your truncation budget.
Color Field Quirks
color is a single 24-bit integer, not an RGB array or hex string:
// Convert hex → integer
const fromHex = (hex) => parseInt(hex.replace('#', ''), 16);
const embed = { color: fromHex('#5865f2') }; // 5793266
Setting color: 0 makes the embed transparent (no left bar). Use color: 1 for “black-ish” if you want the bar.
Field Inline Layout
inline: true lets up to 3 fields share a row. Discord auto-wraps:
"fields": [
{ "name": "CPU", "value": "12%", "inline": true },
{ "name": "Mem", "value": "440 MB", "inline": true },
{ "name": "Disk", "value": "67%", "inline": true },
{ "name": "Region", "value": "us-east", "inline": true }
]
CPU/Mem/Disk go on one row; Region wraps to the next. Long inline values still cause wrapping — keep them under ~30 chars for clean grids.
Quick Validation in the Browser
If you’re prototyping, our Discord Webhook builder shows the live character count for every field and warns before send if a limit is broken. Pair with the embed builder guide and the colors reference.
References
- Discord docs: Embed Object
- Discord docs: Embed Limits
Try it in our tool
Open Discord Webhook Builder