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.
- 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
| Method | When to Use | Time to Implement | Risk |
|---|---|---|---|
| Exponential backoff + jitter in client | 429 rate limit on burst sends | 30–60 min | Low — purely additive |
| API key rotation with scoped permissions | 401/403 or compromised key | 5–10 min | Low — old key must be deleted after rotation |
| Upgrade SendGrid plan or enable IP Pools | Sustained throughput above free/Essentials limits | 15 min + billing change | Medium — verify new limits before removing throttle |
| Whitelist SendGrid IP ranges in WAF/firewall | Webhooks failing or connection refused from SendGrid | 15–30 min | Medium — opens inbound ports; scope to SendGrid CIDRs only |
| Switch to SMTP relay with connection pooling | High-volume transactional sends timing out on HTTP API | 1–2 hours | Medium — requires SMTP credentials and library change |
| Enable Event Webhook signed payload verification | Webhook endpoint receiving forged or duplicate events | 30 min | Low — adds ECDSA signature validation |
| Implement a send queue with Redis/BullMQ | Long-term fix for sustained rate limit violations | 2–4 hours | Low — 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 windowX-RateLimit-Remaining— calls left in current windowX-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)
- Navigate to Settings → API Keys in the SendGrid dashboard.
- Create a new Restricted Access key with only the scopes your application uses.
- Delete the old key immediately after deploying the new one.
- Verify the key works:
curl -H "Authorization: Bearer $NEW_KEY" https://api.sendgrid.com/v3/user/profile - 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:
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}]'Whitelist SendGrid's IP ranges in your WAF, security group, or nginx
allowdirectives. Current ranges are published at https://sendgrid.com/en-us/blog/sending-server-ip-addresses. At time of writing, key CIDR blocks include167.89.0.0/17and208.115.214.0/22— always pull the current list programmatically.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.
Enable signed webhook verification in the SendGrid UI (Settings → Mail Settings → Event Webhook) and validate the
X-Twilio-Email-Event-Webhook-Signatureheader: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)Inspect the Event Webhook logs in the SendGrid dashboard under Activity → Event Webhook to see response codes and failure reasons.
Frequently Asked Questions
#!/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."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
- https://docs.sendgrid.com/api-reference/mail-send/mail-send
- https://docs.sendgrid.com/for-developers/sending-email/smtp-errors-and-troubleshooting
- https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook
- https://docs.sendgrid.com/ui/account-and-settings/api-keys
- https://status.sendgrid.com
- https://stackoverflow.com/questions/tagged/sendgrid
- https://github.com/sendgrid/sendgrid-python/issues