Instagram Reels: lint hashtags and @ tags before the API errors
Meta caps a caption at 2,200 characters, 30 hashtags and 20 @ tags, and the API now returns separate errors. A 20-line check to run before you publish.

A Reel caption sent through the Instagram API may hold at most 2,200 characters, 30 hashtags and 20 @ tags. Since Meta's 2026-09-28 changelog entry, the content publishing flow returns distinct errors when the caption is too long (with the length and the maximum) and when it has too many hashtags or tags too many users. You can catch all three before the request with a small local check.
Limits are from Meta's IG User Media reference, and the errors from the Instagram Platform changelog, both read 2026-10-02. Sume does not write or send captions to Instagram, so the check lives in your publisher.
What are the caption limits?
Meta's description of the caption parameter: it may include hashtags and usernames, and the maximum is 2,200 characters, 30 hashtags and 20 @ tags. It is not supported on images or videos in carousels. The Reel itself is a single container, so the caption applies directly.
Counting is where bugs happen. An emoji or an accented letter can be more than one code unit in some languages, and a hashtag inside a URL fragment can look like a tag to a naive regex. Count what Instagram will parse: # followed by word characters, and @ followed by a username.
| Limit | Value |
|---|---|
| Characters | 2,200 |
| Hashtags | 30 |
| @ tags | 20 |
| Inside carousel children | Not supported |
What do the new errors look like?
The changelog lists, among new content publishing errors: the caption is too long, with the error now reporting the length and the maximum allowed; and the caption contains too many hashtags, or the request tags too many users. They are meant to replace a generic failure so you can tell which field to fix.
A separate non-blocking path exists too: a publish can succeed while part of the request is dropped, and the response then includes config_issue, such as CAPTION_NOT_ATTACHED. A lint step reduces how often you reach that path, but you should still read the field; see config_issue and caption not attached.
What does the check look like?
This script counts characters, hashtags and mentions and prints what to fix. It uses no network, so it runs anywhere Python does.
import re, sys
MAX_CHARS, MAX_TAGS, MAX_MENTIONS = 2200, 30, 20
def lint(caption: str) -> list[str]:
problems = []
tags = re.findall(r"(?<!\w)#\w+", caption)
mentions = re.findall(r"(?<!\w)@[\w.]+", caption)
if len(caption) > MAX_CHARS:
problems.append(f"{len(caption)} chars, max {MAX_CHARS}")
if len(tags) > MAX_TAGS:
problems.append(f"{len(tags)} hashtags, max {MAX_TAGS}")
if len(mentions) > MAX_MENTIONS:
problems.append(f"{len(mentions)} mentions, max {MAX_MENTIONS}")
return problems
if __name__ == "__main__":
found = lint(sys.stdin.read())
print("\n".join(found) or "ok")
sys.exit(1 if found else 0)Where does this sit in a bulk run?
Run the lint when the job is queued, not when the publisher runs, so a bad caption fails minutes before the video is even ready. With Sume the clip arrives from a job, and your caption is a plain string you already hold at that point.
Also watch the account's daily publishing quota: see the daily publishing limit. A caption error that you retry blindly still burns attempts and time, while a lint rejects it for free.
Which edge cases should the lint cover?
Add tests for the cases that bite: a caption of exactly 2,200 characters, one with 31 hashtags, one with 21 mentions, and one with an email address (which contains an @ but is not a mention; the regex above skips it only because of the lookbehind on word characters). Add a line break case too, since newlines count as characters.
If you publish in several languages, run the lint on the final text, not the template, because translation changes length. Korean and German captions in particular can differ from the English source by a wide margin.
Make the lint's output machine-readable if you run a queue: return the list of problems as strings, store them on the job, and show them to whoever wrote the caption. A rejection that says '34 hashtags, max 30' is fixable in seconds; a generic publish failure an hour later is not.
Sources
Related posts
More in Developers
- Instagram Reels impressions metric missing: use views instead
Instagram's insights reference says impressions is deprecated for media created after July 2, 2024. For Reels, views and reach are the replacements to read.
- Is there an Instagram webhook when a Reel finishes processing?
Instagram's webhook field list has no Reel-finished event, so poll status_code. Sume job webhooks are signed; verify them with a secret that cannot be empty.
- Instagram Reels resumable upload (rupload) vs a public video_url
Instagram Reels can publish from a public video_url or a resumable rupload. Resumable is Facebook Login only; check your Sume media link is reachable first.
- reels_skip_rate: what the 3-second metric means and how to test hooks
reels_skip_rate is the share of Reel views that skipped in the first 3 seconds. Pull the opening frames and words with Sume video inspect to compare hooks.
Written by Sume