Diagnosing Twitter API v1.1 Rate Limits and Character-Count Failures
A diagnostic guide for developers hitting Twitter API v1.1's 280-code-point tweet limit and 90-request/15-minute rate ceiling. Includes cause table, ordered checks, code-level fixes for truncation and 429 handling, and escalation triggers.
18 Sept 2025, 00:57 UTC

The Problem: Silent Truncation and Sudden 429 Responses
Applications posting to Twitter via API v1.1 often encounter two distinct failure modes: tweets silently truncated at 280 Unicode code points, and HTTP 429 responses that halt publishing entirely. Both stem from platform-enforced limits that differ by account tier and have shifted since the 2022 ownership change. This guide walks through diagnosing which limit you hit, verifying the current threshold, and adjusting your client logic accordingly.
Quick Diagnostic Table
| Symptom | Likely Cause | Account Tier Affected | Immediate Indicator |
|---|---|---|---|
| Tweet text cut off mid-sentence | Exceeded 280-code-point limit | Standard (non-Blue) | No API error; posted tweet shorter than input |
HTTP 429 on POST statuses/update | Exceeded 90 requests / 15-min window | All authenticated users | Retry-After header present in response |
| Long tweet accepted but truncated in timeline | Client-side counting mismatch (graphemes vs. code points) | Standard | Character counter shows <280 but API rejects |
| 429 after far fewer than 90 requests | Rate-limit window tightened post-acquisition | All tiers | Retry-After value < 900 seconds |
Ordered Checks
- Verify account tier. In the Twitter web UI, open the composing account's profile. A blue checkmark next to the handle indicates a verified (formerly Blue) account eligible for 4,000-code-point tweets. Absence means the 280-code-point cap applies.
- Count code points, not characters. Use
String.prototype.codePointCount()(JS) orlen(text.encode('utf-32-le')) // 4(Python) on the exact payload sent tostatuses/update. Emoji, ZWJ sequences, and regional indicators often occupy multiple code points. - Inspect the last API response. Capture raw HTTP headers for the most recent
POST statuses/update. A429withRetry-After: 900confirms the 15-minute window limit. A200with a returnedtextfield shorter than your input confirms silent truncation. - Check media attachment accounting. Research indicates images add 0 code points to the limit, but video metadata may affect display length. If you attach video, strip the
media_idsparameter and retry with text only to isolate the cause. - Measure actual request rate. Log timestamps of every
statuses/updatecall per access token. Plot a rolling 15-minute count. If the curve approaches 90, you are near the documented ceiling; if it plateaus lower, the effective limit has been reduced.
Fixes Tied to Findings
Finding: Input exceeds 280 code points on a standard account
Implement server-side truncation before the API call, preserving grapheme clusters. Example in Node.js:
import { GraphemeSplitter } from 'grapheme-splitter';
const splitter = new GraphemeSplitter();
function truncateTo280(text) {
const clusters = splitter.splitGraphemes(text);
let count = 0, result = [];
for (const c of clusters) {
const cp = [...c].length; // code points in this cluster
if (count + cp > 280) break;
count += cp;
result.push(c);
}
return result.join('');
}
Run this in your application layer before signing the OAuth request. Requires no elevated permissions. Risk: truncation may split URLs or mentions; consider a secondary pass that backs up to the nearest whitespace.
Finding: HTTP 429 with Retry-After header
Implement exponential backoff keyed to the Retry-After value. Pseudocode:
async function postWithBackoff(payload, token) {
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const resp = await fetch('https://api.twitter.com/1.1/statuses/update.json', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ status: payload })
});
if (resp.status === 200) return resp.json();
if (resp.status === 429) {
const wait = parseInt(resp.headers.get('Retry-After') || '900', 10) * 1000;
await new Promise(r => setTimeout(r, wait));
continue;
}
throw new Error(`Unexpected ${resp.status}: ${await resp.text()}`);
}
throw new Error('Max retries exceeded');
}
Run in any service that holds the user's OAuth 1.0a or Bearer token. Requires network egress to api.twitter.com. Risk: long waits may block job queues; consider offloading to a background worker with persistence.
Finding: Effective rate limit below 90/15min
Throttle proactively. Maintain a token-bucket per access token: capacity = observed max (e.g., 60), refill = capacity / 900 seconds. Before each statuses/update, consume one token; if empty, sleep until refill. This avoids 429s entirely and smooths traffic.
Escalation Criteria
- If
Retry-Afterexceeds 3,600 seconds (1 hour) repeatedly, the account may be flagged for spam review. Pause automation and review Twitter's automation rules. - If verified (Blue) accounts still truncate at 280 code points, the subscription may have lapsed or the API endpoint may not yet honor the 4,000 limit. Confirm subscription status in the developer portal and test with a manual 3,000-code-point tweet via the web UI.
- If media uploads (
media/upload) succeed butstatuses/updatefails with a non-429 error, the media ID may have expired (typical TTL ~ 1 hour). Re-upload and retry immediately.
Limitations and Verification
Rate-limit values have been adjusted multiple times since the 2022 acquisition; the 90/15min figure is the last widely documented default but may not reflect current enforcement. The only reliable verification is empirical: run a controlled burst from a test account and record the exact request count at first 429. Character-count behavior for complex scripts (Devanagari conjuncts, emoji ZWJ sequences) depends on Twitter's internal grapheme segmentation, which is not publicly specified. Test each script family your application supports.
Practical verification step: from a standard account, send POST statuses/update with a 300-code-point payload. Confirm the response is 200 but the returned text length is 280. From a verified account, send 3,500 code points; confirm the full text appears in the response. Document the observed limits in your runbook and re-validate quarterly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.