Twilio Error Handling: Common Errors, Retries, and Debugging Guide
In this tutorial, you will learn about Twilio Error Handling: Common Errors, Retries, and Debugging Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Twilio API errors include HTTP status codes (400, 401, 429, 500), application-specific error codes (20001-63018), and Webhook delivery failures, requiring structured error handling, retry logic, and monitoring.
What You'll Learn
How to handle common Twilio API errors, understand Twilio error codes, implement retry strategies for transient failures, debug webhook errors, use the Twilio Debugger for troubleshooting, and set up proactive monitoring.
Why It Matters
Unhandled errors cause message failures, lost revenue, and poor user experience. DodaTech's error handling catches Twilio errors at every layer, retries transient failures, and alerts on anomalies to maintain 99.9% message delivery.
Real-World Use
A DodaTech SMS fails with error 21610 (unreachable carrier). The retry system waits 10 seconds, attempts from a different number in the Messaging Service pool, succeeds, and logs the incident for carrier review.
flowchart LR
A["API Call"] --> B{"HTTP\nStatus"}
B -->|200| C["Success"]
B -->|429| D["Rate Limited\nRetry"]
B -->|401| E["Auth Error\nCheck Keys"]
B -->|400| F["Bad Request\nCheck Params"]
D --> G["Exponential\nBackoff"]
G --> A
B -->|500| H["Twilio Error\nRetry"]
style C fill:#bbf7d0,stroke:#16a34a
style D fill:#fef3c7,stroke:#d97706
style E fill:#fecaca,stroke:#dc2626
style H fill:#dbeafe,stroke:#2563eb
Catching Twilio API Errors
import os
import time
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException
client = Client(
os.environ["TWILIO_ACCOUNT_SID"],
os.environ["TWILIO_AUTH_TOKEN"]
)
def send_message_with_retry(to_number, body, max_retries=3):
last_error = None
for attempt in range(max_retries):
try:
message = client.messages.create(
body=body,
from_=os.environ["TWILIO_PHONE_NUMBER"],
to=to_number
)
print(f"Sent: {message.sid} (attempt {attempt+1})")
return message
except TwilioRestException as e:
last_error = e
print(f"Attempt {attempt+1} failed:")
print(f" Code: {e.code}")
print(f" Status: {e.status}")
print(f" Message: {e.msg}")
if e.code in [20003, 20012, 20429, 21610]:
# Retryable errors
wait = 2 ** attempt # Exponential backoff: 1, 2, 4
print(f" Retrying in {wait}s...")
time.sleep(wait)
else:
# Non-retryable, fail immediately
print(" Fatal error, not retrying")
raise
print(f"Failed after {max_retries} attempts")
return None
# send_message_with_retry("+14155551234", "Hello!")
Common Twilio Error Codes
# Most frequent Twilio error codes
error_codes = {
20001: "Account not ready for this action",
20003: "Authentication failure - check Account SID and Auth Token",
20012: "Account suspended or closed",
20429: "Rate limit exceeded - slow down requests",
21211: "Invalid 'To' phone number format",
21212: "Invalid 'From' phone number - call from an owned number",
21214: "Message body is required",
21401: "Invalid 'From' number - messaging service not configured",
21408: "Permission not enabled for this country",
21602: "Message body too long (>1600 chars)",
21610: "Unreachable carrier - phone cannot receive SMS",
21611: "Message has been blocked by carrier",
21612: "Landline or unreachable carrier",
30001: "Queue overflow - too many messages",
30002: "Account message limit exceeded",
30003: "Too many messages to a single number",
63018: "WhatsApp template not approved",
}
def get_error_advice(error_code):
advice = error_codes.get(error_code, "Unknown error - check Twilio docs")
retryable = error_code in [20429, 21610, 30001, 30002, 30003]
print(f"Error {error_code}: {advice}")
print(f"Retryable: {retryable}")
return advice
# get_error_advice(20429)
# Expected output:
# Error 20429: Rate limit exceeded - slow down requests
# Retryable: True
Handling Rate Limiting
import time
from threading import Lock
class TwilioRateLimiter:
"""Simple rate limiter for Twilio API calls."""
def __init__(self, max_per_second=50):
self.max_per_second = max_per_second
self.tokens = max_per_second
self.lock = Lock()
self.last_refill = time.time()
def _refill(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.max_per_second,
self.tokens + elapsed * self.max_per_second)
self.last_refill = now
def acquire(self):
while True:
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return
time.sleep(0.01)
limiter = TwilioRateLimiter(max_per_second=50)
def rate_limited_send(to_number, body):
limiter.acquire()
return client.messages.create(
body=body,
from_=os.environ["TWILIO_PHONE_NUMBER"],
to=to_number
)
# Test: send 10 messages quickly
# for i in range(10):
# rate_limited_send("+14155551234", f"Test message {i+1}")
Webhook Error Handling
from flask import Flask, request, Response
from twilio.twiml.messaging_response import MessagingResponse
app = Flask(__name__)
@app.route("/sms/status", methods=["POST"])
def handle_status_callback():
"""Handle delivery status updates and errors."""
message_sid = request.form.get("MessageSid")
message_status = request.form.get("MessageStatus")
error_code = request.form.get("ErrorCode", "0")
error_message = request.form.get("ErrorMessage", "")
print(f"Status: {message_sid} -> {message_status}")
if error_code and error_code != "0":
print(f"ERROR {error_code}: {error_message}")
# Log to monitoring system
log_error(message_sid, error_code, error_message)
# Trigger retry for specific errors
if error_code in ["21610", "21611", "30003"]:
retry_with_different_number(message_sid)
return Response("", status=200)
def log_error(message_sid, error_code, error_message):
print(f"[ERROR LOG] {message_sid}: {error_code} - {error_message}")
def retry_with_different_number(message_sid):
print(f"Retrying {message_sid} with different sender...")
# Implementation depends on your retry logic
Using the Twilio Debugger
# Programmatic access to Debugger events
def get_debugger_events(limit=10):
"""Fetch recent Twilio Debugger events."""
events = client.monitor.events.list(
limit=limit,
source_ip="*"
)
print(f"Recent debugger events ({len(events)}):")
for event in events:
print(f" [{event.date_created}] {event.description}")
print(f" SID: {event.sid}")
print(f" Level: {event.level}")
return events
# get_debugger_events()
# Expected output:
# Recent debugger events (3):
# [2026-06-28 10:30:00] Error 21610 - Unreachable carrier
# SID: AExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Level: ERROR
# [2026-06-28 10:25:00] Warning - SMS to landline
# SID: AEyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
# Level: WARNING
Common Mistakes
1. Not Distinguishing Retryable vs Non-Retryable Errors
Errors 20003 (auth failure) and 21211 (invalid number) will never succeed on retry. Only retry transient errors (20429 rate limit, 21610 unreachable carrier, 30001 queue overflow).
2. Immediate Retry Without Backoff
Retrying immediately on rate limit (429) makes the problem worse. Use exponential backoff with jitter. Wait 1s, 2s, 4s, 8s between retries with random jitter.
3. Ignoring Status Callback Errors
Status callbacks contain delivery failure details. If you don't handle undelivered and failed statuses, you won't know messages didn't arrive. Always implement status callback handlers.
4. Not Setting HTTP Timeouts
Default HTTP timeouts can be 30+ seconds. Set explicit timeouts (5-10s) on all Twilio API calls. Long-running calls block your application threads and cause cascading failures.
5. Skipping Signature Validation
Webhook endpoints without Twilio signature validation are vulnerable to spoofing. An attacker can POST fake status updates. Always validate X-Twilio-Signature on webhook endpoints.
Practice Questions
- What HTTP status code does Twilio return when rate limited?
- Which errors should you retry and which should you fail immediately?
- How do you access Twilio error details in the Python SDK?
- What is the Twilio Debugger and how do you use it?
Answers:
- HTTP 429 (Too Many Requests) with error code 20429. The response includes a
Retry-Afterheader with the recommended wait time. - Retry: 20429 (rate limit), 21610 (unreachable carrier), 30001-30003 (queue/limit issues). Do not retry: 20003 (auth), 21211 (invalid number), 21212 (invalid from), 21401 (unowned number).
- Catch
TwilioRestException. Accesse.code(integer error code),e.status(HTTP status),e.msg(human-readable message), ande.uri(API endpoint). - The Debugger is a Console tool that captures errors, warnings, and notable events. Access via Console > Monitor > Debugger. Also accessible via the Monitor API for programmatic analysis.
Challenge: Build a comprehensive error handling system: implement retry logic with exponential backoff for all SMS sending, categorize errors as retryable/fatal, set up a status callback endpoint that logs all delivery failures, integrate with the Twilio Debugger API for alerting, create a rate limiter for high-volume sending, and generate a weekly error report.
FAQ
Mini Project
Build an error monitoring system: implement a rate-limited SMS sender with exponential backoff retry, create a status callback handler that categorizes errors, integrate the Debugger API for log aggregation, build a dashboard showing success rate, error distribution, and retry statistics, and configure alerts for high error rates.
What's Next
Credentials Security — secure your Twilio API keys and credentials.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro