Skip to content

Twilio Error Handling: Common Errors, Retries, and Debugging Guide

DodaTech Updated 2026-06-28 7 min read

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

  1. What HTTP status code does Twilio return when rate limited?
  2. Which errors should you retry and which should you fail immediately?
  3. How do you access Twilio error details in the Python SDK?
  4. What is the Twilio Debugger and how do you use it?

Answers:

  1. HTTP 429 (Too Many Requests) with error code 20429. The response includes a Retry-After header with the recommended wait time.
  2. 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).
  3. Catch TwilioRestException. Access e.code (integer error code), e.status (HTTP status), e.msg (human-readable message), and e.uri (API endpoint).
  4. 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

What is the most common Twilio error?

Error 21610 (unreachable carrier) is most common — the phone number cannot receive SMS. This happens with landlines, VoIP numbers, or numbers that have been disconnected.

How many times should I retry a failed message?

Three retries with exponential backoff (1s, 2s, 4s) is standard. For critical messages, extend to 5 retries with longer intervals. Track retry counts to avoid infinite loops.

What does error 30002 mean?

Account message limit exceeded. Every Twilio account has a message throughput limit (default ~1 msg/sec per number, higher for Messaging Services). Wait or request a limit increase.

How do I check if a number can receive SMS before sending?

Use the Twilio Lookup API with type: carrier. It returns the line type (mobile, landline, VoIP). Only send SMS to mobile numbers to avoid 21610 errors.

What is the best way to monitor Twilio errors in production?

Use status callbacks for real-time delivery tracking, the Debugger API for error aggregation, and a monitoring service (Datadog, New Relic) with alerts for spikes in error codes 30001-30003.

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