Python Twilio SMS — Deep Dive

System-level framing

At scale, Twilio SMS integration is less about the API call and more about the system around it: webhook reliability, opt-out compliance, cost tracking, rate limiting, and failure recovery. The Twilio Python SDK handles the easy part — the engineering challenge is everything else.

Sending messages: beyond the basics

Standard send

from twilio.rest import Client
import os

client = Client(
    os.environ['TWILIO_ACCOUNT_SID'],
    os.environ['TWILIO_AUTH_TOKEN']
)

message = client.messages.create(
    body="Your verification code is 847293. It expires in 10 minutes.",
    from_=os.environ['TWILIO_PHONE_NUMBER'],
    to="+14155551234",
    status_callback="https://myapp.com/webhooks/sms-status"
)

print(f"SID: {message.sid}, Status: {message.status}")
message = client.messages.create(
    body="Your order #12345 has shipped!",
    messaging_service_sid=os.environ['TWILIO_MESSAGING_SERVICE_SID'],
    to="+14155551234",
    status_callback="https://myapp.com/webhooks/sms-status"
)

Messaging Services handle number rotation, sticky sender (same recipient always gets messages from the same number), and compliance checks automatically.

Webhook handling

Delivery status webhooks

from flask import Flask, request
from twilio.request_validator import RequestValidator

app = Flask(__name__)
validator = RequestValidator(os.environ['TWILIO_AUTH_TOKEN'])

@app.route('/webhooks/sms-status', methods=['POST'])
def sms_status():
    # Validate the request actually came from Twilio
    url = request.url
    params = request.form.to_dict()
    signature = request.headers.get('X-Twilio-Signature', '')
    
    if not validator.validate(url, params, signature):
        return 'Unauthorized', 403
    
    message_sid = params['MessageSid']
    status = params['MessageStatus']
    error_code = params.get('ErrorCode')
    
    # Update your database
    update_message_status(message_sid, status, error_code)
    
    return '', 204

Always validate the X-Twilio-Signature header. Without this, anyone can POST fake status updates to your webhook endpoint.

Receiving inbound SMS

from twilio.twiml.messaging_response import MessagingResponse

@app.route('/webhooks/sms-inbound', methods=['POST'])
def inbound_sms():
    from_number = request.form['From']
    body = request.form['Body'].strip().upper()
    
    resp = MessagingResponse()
    
    if body == 'STOP':
        add_to_opt_out_list(from_number)
        resp.message("You've been unsubscribed. Reply START to re-subscribe.")
    elif body == 'STATUS':
        order = get_latest_order(from_number)
        resp.message(f"Order #{order.id}: {order.status}")
    else:
        resp.message("Reply STATUS to check your order, or STOP to unsubscribe.")
    
    return str(resp), 200, {'Content-Type': 'text/xml'}

Rate limiting and queuing

Carrier rate limits

Carriers impose throughput limits that vary by number type:

Number typeMessages per secondDaily limit
Local (10DLC)1-75 MPS (varies by trust score)No hard limit
Toll-free3 MPSNo hard limit
Short code100+ MPSNo hard limit

Exceeding these limits causes message queuing on Twilio’s side (adding latency) or carrier filtering (messages silently dropped).

Application-side rate limiting

import time
from collections import deque
from threading import Lock

class SMSRateLimiter:
    def __init__(self, max_per_second: float = 10):
        self.max_per_second = max_per_second
        self.timestamps = deque()
        self.lock = Lock()
    
    def wait_if_needed(self):
        with self.lock:
            now = time.monotonic()
            # Remove timestamps older than 1 second
            while self.timestamps and now - self.timestamps[0] > 1.0:
                self.timestamps.popleft()
            
            if len(self.timestamps) >= self.max_per_second:
                sleep_time = 1.0 - (now - self.timestamps[0])
                if sleep_time > 0:
                    time.sleep(sleep_time)
            
            self.timestamps.append(time.monotonic())

limiter = SMSRateLimiter(max_per_second=10)

def send_sms(to: str, body: str):
    limiter.wait_if_needed()
    return client.messages.create(
        body=body,
        messaging_service_sid=os.environ['TWILIO_MESSAGING_SERVICE_SID'],
        to=to
    )

For production bulk sending, use Celery with rate limiting:

from celery import Celery
from celery.utils.log import get_task_logger

app = Celery('sms')
logger = get_task_logger(__name__)

@app.task(
    bind=True,
    rate_limit='10/s',
    max_retries=3,
    default_retry_delay=60
)
def send_sms_task(self, to: str, body: str):
    try:
        message = client.messages.create(
            body=body,
            messaging_service_sid=os.environ['TWILIO_MESSAGING_SERVICE_SID'],
            to=to
        )
        logger.info(f"Sent {message.sid} to {to}")
        return message.sid
    except TwilioRestException as e:
        if e.status == 429:  # Rate limited
            raise self.retry(exc=e, countdown=30)
        raise

Cost optimization

Track spending programmatically

from datetime import datetime, timedelta

# Get today's message count and cost
today = datetime.utcnow().date()
messages = client.messages.list(
    date_sent_after=datetime(today.year, today.month, today.day),
    limit=1000
)

total_cost = sum(float(m.price or 0) for m in messages)
total_segments = sum(m.num_segments for m in messages if m.num_segments)

print(f"Today: {len(messages)} messages, {total_segments} segments, ${abs(total_cost):.2f}")

Cost reduction strategies

  • Keep messages under 160 characters — One segment instead of two or three.
  • Use Twilio’s Content API for templated messages — carriers give these higher trust scores.
  • Batch non-urgent messages — Send appointment reminders in a scheduled batch rather than on-demand.
  • Set spending alerts in the Twilio console to catch runaway costs early.
  • Use short URLs — Replace long tracking URLs with a shortener to stay under 160 characters.

Compliance patterns

TCPA and opt-out handling

US law (Telephone Consumer Protection Act) requires:

  • Prior express consent before sending marketing SMS
  • Immediate opt-out processing when a user replies STOP
  • Clear identification of who is sending the message

Twilio’s Advanced Opt-Out feature handles STOP/START/HELP keywords automatically for Messaging Services. For custom handling:

# Maintain an opt-out list
def is_opted_out(phone_number: str) -> bool:
    return db.opt_outs.exists(phone=phone_number)

def send_if_allowed(to: str, body: str) -> str | None:
    if is_opted_out(to):
        logger.info(f"Skipping opted-out number: {to}")
        return None
    return send_sms(to, body)

A2P 10DLC registration flow

  1. Register your brand with Twilio (company name, EIN, website).
  2. Create a messaging campaign (use case, sample messages, opt-in flow).
  3. Twilio submits to The Campaign Registry (TCR).
  4. Wait for approval (hours to days).
  5. Associate your Twilio phone numbers with the approved campaign.
  6. Send messages — carriers now trust your traffic.

Without this, US carriers will throttle or block your messages. Trust scores determine your throughput: higher scores (based on brand reputation) allow more messages per second.

Error handling taxonomy

Error codeMeaningAction
21211Invalid phone numberRemove from contact list
21608Number not owned by your accountCheck from_ parameter
21610Recipient opted outHonor opt-out, do not retry
21614Number incapable of receiving SMSTry voice or email instead
30003Unreachable handsetRetry later (phone may be off)
30004Message blocked by carrierCheck content for spam triggers
30006Landline or unreachableRemove from SMS list
30007Message filteringImprove 10DLC trust score
from twilio.base.exceptions import TwilioRestException

PERMANENT_ERRORS = {21211, 21610, 21614, 30006}

def send_with_error_handling(to: str, body: str):
    try:
        return client.messages.create(
            body=body,
            messaging_service_sid=os.environ['TWILIO_MESSAGING_SERVICE_SID'],
            to=to
        )
    except TwilioRestException as e:
        if e.code in PERMANENT_ERRORS:
            mark_number_invalid(to, e.code)
            return None
        raise  # Transient errors bubble up for retry

The one thing to remember: Production SMS with Twilio is 10% API integration and 90% operational infrastructure — rate limiting, compliance registration, cost tracking, and graceful error handling determine whether your system scales or gets blocked by carriers.

pythontwiliosmsmessaging

See Also

  • Python Discord Bot Development Learn how Python creates Discord bots that moderate servers, play music, and respond to commands — explained for total beginners.
  • Python Email Templating Jinja Discover how Jinja templates let Python create personalized emails for thousands of people without writing each one by hand.
  • Python Imap Reading Emails See how Python reads your inbox using IMAP — explained with a mailbox-and-key analogy anyone can follow.
  • Python Push Notifications How Python sends those buzzing alerts to your phone and browser — explained for anyone who has ever wondered where notifications come from.
  • Python Slack Bot Development Find out how Python builds Slack bots that read messages, reply to commands, and automate team workflows — no Slack expertise needed.