Performance Max text limits: 30, 90, 90 and 25 characters, counted
Performance Max headlines run 30 characters, long headlines 90, descriptions 90, business name 25. A short script counts generated copy before upload.

Performance Max asset groups accept headlines of up to 30 characters (3 to 15 of them), long headlines of up to 90 (1 to 5), descriptions of up to 90 (2 to 5) and a business name of up to 25. Count generated copy against those limits in code before you submit it, because a slot over its limit fails the format.
The text slots
The Google Ads API page on Performance Max asset requirements lists these slots. Values were read on 2026-10-03.
| Slot | Max characters | How many |
|---|---|---|
| Headline | 30 | 3 to 15 |
| Long headline | 90 | 1 to 5 |
| Description | 90 | 2 to 5 |
| Business name | 25 | 1 |
Pair copy with visuals as a unit
A creative review goes faster when the copy slot and the visual that sits next to it are produced together. A batch that renders ten visuals and a separate sheet of copy invites mismatches, such as a headline that names a product color the image does not show.
One approach is to generate the visual from a structured brief and write the headline from the same brief. With Sume, a Format run item can bind an output_schema, so the run returns copy fields in a fixed shape. Bulk runs queue 1 to 100 such items with a concurrency window of 1 to 16.
A counting script
The script below checks a list of candidates against each slot and prints what to cut. It counts Python characters, so confirm how Google counts any non-Latin script or emoji in your account before relying on it for those.
LIMITS = {
"headline": (30, 3, 15),
"long_headline": (90, 1, 5),
"description": (90, 2, 5),
"business_name": (25, 1, 1),
}
def check(slot, items):
max_len, min_n, max_n = LIMITS[slot]
problems = []
if not (min_n <= len(items) <= max_n):
problems.append(f"{slot}: need {min_n}-{max_n} items, have {len(items)}")
for text in items:
if len(text) > max_len:
problems.append(f"{slot}: {len(text)}/{max_len} -> {text!r}")
return problems
copy = {
"headline": ["Trail shoes built to grip", "Light on the foot", "Free returns for 30 days"],
"long_headline": ["Lightweight trail running shoes with a grippy sole for wet ground"],
"description": [
"Cushioned, light and built for wet trails.",
"Order today and try them for 30 days.",
],
"business_name": ["Northline Outfitters"],
}
for slot, items in copy.items():
for line in check(slot, items):
print(line)
print("done")Where generated copy goes wrong
Language models overshoot character limits often, and asking for a limit in the prompt does not guarantee it. Keep the check as a gate in the pipeline, not as a hope. When a line is over, rewrite it and recheck rather than truncating mid-word.
Keep claims in the copy consistent with the product the visual shows. Anything about price, discounts or shipping must match your landing page, which is a policy matter on the ad platform and not something a character counter can verify.
Sources
Related posts
More in Developers
- Push or poll for a finished render: listen, webhook or jobs_wait
MCP 2026-07-28 adds subscriptions/listen. For a render that takes minutes, compare a listen stream, a signed webhook and jobs_wait, with a Python verifier.
- Python 3.10 is end of life: a stdlib Sume webhook verifier
Python 3.10 has reached end of life. A standard-library verifier for Sume's signed webhooks that refuses an empty secret and accepts rotated signatures.
- Recraft V4.1 Flash: median 1.3 s, p95 1.8 s. Set timeouts from p95
Recraft quotes a median of about 1.3 seconds and a p95 of 1.8 seconds for V4.1 Flash. How to turn latency claims into timeouts and polling for image APIs.
- Reproduce the same AI voiceover later: model id, voice, settings
To redo a narration line months later you need the model id, voice, language, format and settings. Sume's completed TTS job records them. A short routine.
Written by Sume