Error Medic

SendGrid Rate Limit Exceeded (429) & Authentication Errors: Complete Troubleshooting Guide

Fix SendGrid rate limit 429, 401, 403, 502, timeout, and webhook errors with step-by-step commands, code examples, and root cause analysis.

Last updated:
Last verified:
2,013 words
Key Takeaways
  • HTTP 429 rate limit errors occur when your API call volume exceeds your SendGrid plan's per-minute or per-day ceiling — implement exponential backoff and request queuing immediately
  • 401 Unauthorized and 403 Forbidden errors almost always trace to a malformed, revoked, or insufficiently-scoped API key; regenerate with least-privilege scopes and verify the Authorization header format
  • 502 Bad Gateway and connection timeouts are usually transient SendGrid infrastructure events — check status.sendgrid.com first, then audit your retry logic and connection pool settings
  • Webhook delivery failures stem from three sources: your endpoint returning non-2xx, firewall rules blocking SendGrid's IP ranges, or missing HTTPS/TLS on the receiving server
  • Quick fix path: (1) confirm your plan limits at app.sendgrid.com, (2) rotate your API key, (3) add retry-with-backoff, (4) whitelist SendGrid CIDR blocks on your firewall
Fix Approaches Compared
MethodWhen to UseTime to ImplementRisk
Exponential backoff + jitter in client429 rate limit on burst sends30–60 minLow — purely additive
API key rotation with scoped permissions401/403 or compromised key5–10 minLow — old key must be deleted after rotation
Upgrade SendGrid plan or enable IP PoolsSustained throughput above free/Essentials limits15 min + billing changeMedium — verify new limits before removing throttle
Whitelist SendGrid IP ranges in WAF/firewallWebhooks failing or connection refused from SendGrid15–30 minMedium — opens inbound ports; scope to SendGrid CIDRs only
Switch to SMTP relay with connection poolingHigh-volume transactional sends timing out on HTTP API1–2 hoursMedium — requires SMTP credentials and library change
Enable Event Webhook signed payload verificationWebhook endpoint receiving forged or duplicate events30 minLow — adds ECDSA signature validation
Implement a send queue with Redis/BullMQLong-term fix for sustained rate limit violations2–4 hoursLow — decouples send rate from application traffic

Understanding SendGrid Errors

SendGrid's Web API v3 enforces several independent rate limits that developers frequently conflate. Understanding which limit you have hit is the first diagnostic step.

Plan-level daily send limits govern how many email messages you can send per day. Free accounts cap at 100 emails/day. Essentials starts at 40,000/month with a per-second burst ceiling. Violating these produces:

HTTP 429 Too Many Requests
{"errors":[{"message":"too many requests","field":null,"help":null}]}

API request rate limits are separate. The Web API v3 allows approximately 600 API calls per minute regardless of plan. High-frequency status polling or validation calls hit this limit even when message volume is low.

SMTP rate limits apply to the SMTP relay path: 100 connections per 10-second window, with a maximum of 1,000 concurrent connections on Pro plans.


Step 1: Identify the Exact Error Code

Before touching any configuration, capture the full HTTP response — status code, headers, and body. The X-RateLimit-Remaining and X-RateLimit-Reset headers tell you exactly when your window resets.

curl -s -D - -o /dev/null \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" \
  https://api.sendgrid.com/v3/mail/send \
  --data '{"personalizations":[{"to":[{"email":"test@example.com"}]}],"from":{"email":"from@example.com"},"subject":"test","content":[{"type":"text/plain","value":"test"}]}'

Key headers to inspect:

  • X-RateLimit-Limit — your plan ceiling per window
  • X-RateLimit-Remaining — calls left in current window
  • X-RateLimit-Reset — Unix timestamp when window resets

401 Unauthorized means the API key is missing, malformed, or revoked:

{"errors":[{"message":"The provided authorization grant is invalid, expired, or revoked","field":null,"help":null}]}

Check that your Authorization header is exactly Bearer SG.xxxx — not Bearer: SG.xxxx, not API-Key SG.xxxx.

403 Forbidden means the key exists but lacks the required scope. A key created with only Mail Send permissions cannot call /v3/stats or /v3/suppression/bounces:

{"errors":[{"message":"Access forbidden","field":null,"help":null}]}

502 Bad Gateway and 503 Service Unavailable are upstream errors from SendGrid's infrastructure. Always check https://status.sendgrid.com before debugging your own code.


Step 2: Fix Rate Limit Violations (429)

Immediate mitigation — add exponential backoff to your send loop. The following Python example uses tenacity:

import sendgrid
from tenacity import retry, wait_exponential, stop_after_attempt, retry_if_exception_type
from python_http_client.exceptions import HTTPError

@retry(
    retry=retry_if_exception_type(HTTPError),
    wait=wait_exponential(multiplier=1, min=2, max=60),
    stop=stop_after_attempt(5)
)
def send_email(sg_client, message):
    response = sg_client.send(message)
    if response.status_code == 429:
        raise HTTPError(response)
    return response

Long-term mitigation — move sends to a Redis-backed queue. Use BullMQ (Node.js) or Celery (Python) with a rate limiter set to 90% of your plan limit to leave headroom:

// BullMQ rate-limited queue — stays under 500 req/min for safety
const emailQueue = new Queue('email', { connection: redisClient });
await emailQueue.setRateLimiter({ max: 500, duration: 60000 });

Step 3: Fix Authentication Errors (401 / 403)

  1. Navigate to Settings → API Keys in the SendGrid dashboard.
  2. Create a new Restricted Access key with only the scopes your application uses.
  3. Delete the old key immediately after deploying the new one.
  4. Verify the key works: curl -H "Authorization: Bearer $NEW_KEY" https://api.sendgrid.com/v3/user/profile
  5. If you see 403 on a specific endpoint, compare the required scopes in the SendGrid API reference against your key's permissions.

For 403 errors caused by IP Access Management, SendGrid allows you to whitelist source IPs for API calls. If your CI/CD pipeline or serverless function has a dynamic IP, either disable IP Access Management or use a NAT gateway with a static IP.


Step 4: Fix 502/Timeout Errors

502 errors are almost always transient. Build your retry logic to treat any 5xx as retryable:

RETRYABLE_CODES = {429, 500, 502, 503, 504}

def is_retryable(response):
    return response.status_code in RETRYABLE_CODES

For persistent 502s on the SMTP path, verify your connection settings:

  • Use port 587 with STARTTLS (preferred) or port 465 with TLS
  • Server: smtp.sendgrid.com
  • Username: the literal string apikey
  • Password: your API key value (not Base64-encoded)

Connection refused on port 25 is expected — SendGrid blocks outbound port 25 from residential and most cloud IPs. Always use 587.


Step 5: Fix Webhook Delivery Failures

SendGrid's Event Webhook delivers POST requests from a fixed set of IP addresses. If your endpoint returns non-2xx or the requests never arrive, follow this sequence:

  1. Verify endpoint reachability from outside your network:

    curl -X POST https://yourdomain.com/webhook/sendgrid \
      -H "Content-Type: application/json" \
      -d '[{"event":"open","email":"test@example.com","timestamp":1700000000}]'
    
  2. Whitelist SendGrid's IP ranges in your WAF, security group, or nginx allow directives. Current ranges are published at https://sendgrid.com/en-us/blog/sending-server-ip-addresses. At time of writing, key CIDR blocks include 167.89.0.0/17 and 208.115.214.0/22 — always pull the current list programmatically.

  3. Check TLS — SendGrid requires HTTPS. A self-signed certificate will cause silent delivery failures. Use Let's Encrypt if you don't have a commercial cert.

  4. Enable signed webhook verification in the SendGrid UI (Settings → Mail Settings → Event Webhook) and validate the X-Twilio-Email-Event-Webhook-Signature header:

    from sendgrid.helpers.eventwebhook import EventWebhook
    
    ew = EventWebhook()
    ec_public_key = ew.convert_public_key(os.environ['SENDGRID_WEBHOOK_KEY'])
    is_valid = ew.verify_signature(payload, ec_public_key, signature, timestamp)
    
  5. Inspect the Event Webhook logs in the SendGrid dashboard under Activity → Event Webhook to see response codes and failure reasons.

Frequently Asked Questions

bash
#!/usr/bin/env bash
# SendGrid Diagnostic Script
# Usage: SENDGRID_API_KEY=SG.xxx bash sendgrid-diag.sh [your-webhook-url]

set -euo pipefail
API_KEY="${SENDGRID_API_KEY:-}"
WEBHOOK_URL="${1:-}"
BASE="https://api.sendgrid.com/v3"

if [[ -z "$API_KEY" ]]; then
  echo "ERROR: Set SENDGRID_API_KEY environment variable" && exit 1
fi

echo "=== 1. API Key Validation ==="
RESP=$(curl -s -w "\n%{http_code}" \
  -H "Authorization: Bearer $API_KEY" \
  "$BASE/user/profile")
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | head -n -1)
echo "HTTP $HTTP_CODE"
[[ "$HTTP_CODE" == "200" ]] && echo "Key valid: $(echo $BODY | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get(\"username\",\"unknown\"))' 2>/dev/null)" || echo "Key error: $BODY"

echo ""
echo "=== 2. Rate Limit Headers ==="
curl -s -I \
  -H "Authorization: Bearer $API_KEY" \
  "$BASE/mail/send" 2>/dev/null | grep -i -E "x-ratelimit|content-type|http/"

echo ""
echo "=== 3. Current Send Stats ==="
DATE_FROM=$(date -u -d '7 days ago' '+%Y-%m-%d' 2>/dev/null || date -u -v-7d '+%Y-%m-%d')
curl -s \
  -H "Authorization: Bearer $API_KEY" \
  "$BASE/stats?start_date=$DATE_FROM" | python3 -c '
import sys, json
data = json.load(sys.stdin)
total = sum(s["stats"][0]["metrics"]["requests"] for s in data if s.get("stats"))
print(f"Emails sent last 7 days: {total}")'

echo ""
echo "=== 4. Bounce & Suppression Count ==="
curl -s \
  -H "Authorization: Bearer $API_KEY" \
  "$BASE/suppression/bounces?limit=1" -I 2>/dev/null | grep -i "x-list-count" || echo "Bounces endpoint not accessible (check key scopes)"

echo ""
echo "=== 5. SMTP Connectivity ==="
if command -v nc &>/dev/null; then
  nc -zv smtp.sendgrid.com 587 2>&1 && echo "Port 587 OPEN" || echo "Port 587 BLOCKED"
  nc -zv smtp.sendgrid.com 25 2>&1 && echo "Port 25 OPEN" || echo "Port 25 BLOCKED (expected on most networks)"
else
  echo "nc not available; install netcat to test SMTP connectivity"
fi

echo ""
echo "=== 6. Webhook Endpoint Test ==="
if [[ -n "$WEBHOOK_URL" ]]; then
  PAYLOAD='[{"email":"test@example.com","event":"open","timestamp":1700000000,"sg_event_id":"diag-test"}]'
  WH_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "$WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "$PAYLOAD")
  echo "Webhook POST to $WEBHOOK_URL → HTTP $WH_CODE"
  [[ "$WH_CODE" =~ ^2 ]] && echo "Webhook endpoint reachable" || echo "WARNING: Non-2xx response; SendGrid will retry and eventually stop delivering"
else
  echo "No webhook URL provided; skip with: bash sendgrid-diag.sh https://your-domain.com/webhook"
fi

echo ""
echo "=== 7. SendGrid Status Page ==="
STATUS=$(curl -s "https://yqbtkvpvhyvc.statuspage.io/api/v2/status.json" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["status"]["description"])' 2>/dev/null || echo "Could not fetch status")
echo "SendGrid platform status: $STATUS"

echo ""
echo "Diagnostic complete. Share this output when opening a support ticket."
E

Error Medic Editorial

The Error Medic Editorial team comprises senior DevOps engineers, SREs, and cloud architects with collective experience across AWS, GCP, and Azure production environments. We specialize in translating cryptic API errors and platform-specific failure modes into reproducible diagnostic workflows and actionable fixes. Our guides are tested against live environments before publication.

Sources

Related Articles in Sendgrid

Explore More API Errors Guides