Skip to main content
All articles
· Updated April 23, 2026

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.

embed limitsvalidationcharactersfieldspayloadwebhookdiscord api
Discord Embed Limits Cheat Sheet (Characters, Fields & Total Payload)

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

FieldMaxCounts toward total?
title256 charsyes
description4096 charsyes
fields25 entries
field.name256 charsyes
field.value1024 charsyes
footer.text2048 charsyes
author.name256 charsyes
url, image.url, thumbnail.url, author.url, footer.icon_url, author.icon_url2048 charsno
color24-bit integer (0–16777215)
timestampISO-8601

Per-Message Limits

FieldMax
content2000 chars (4000 with Nitro)
embeds10 per message
Total embed text (sum of all title + description + field.name + field.value + footer.text + author.name across all embeds)6000 chars
attachments10 files
File size25 MB free / 50–500 MB tier-dependent

Webhook-Specific

FieldMax
username80 chars (and cannot be “Clyde” or “Discord”)
avatar_url2048 chars
thread_name100 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