SendGrid Rate Limit, 401, 403 & 502 Errors: Complete Troubleshooting Guide
Fix SendGrid rate limit exceeded, 401 authentication failed, 403 forbidden, 502 gateway errors, timeouts, and webhook failures with step-by-step diagnostic comm
- SendGrid 429 rate limit errors occur when you exceed 100 requests/second on the v3 API or breach your plan's monthly send limit — implement exponential backoff with jitter immediately
- 401 Unauthorized and 403 Forbidden errors almost always trace to a missing, revoked, or scope-restricted API key; check the key's permission scopes in the SendGrid dashboard before anything else
- 502 Bad Gateway and connection refused errors are typically transient SendGrid infrastructure issues but can also indicate incorrect API endpoint URLs or TLS/SSL misconfiguration on the client side
- Webhook delivery failures stem from your endpoint returning non-2xx responses, timeouts exceeding 3 seconds, or SSL certificate validation errors on your receiving server
- Implement retry logic with exponential backoff (base 1s, max 64s, jitter ±500ms) for all transient errors (429, 500, 502, 503, 504) to avoid thundering herd problems
| Error | Root Cause | Fix Method | Time to Resolve | Risk |
|---|---|---|---|---|
| 429 Rate Limit | Exceeding 100 req/s or monthly cap | Exponential backoff + queue batching | 1–4 hours (code change) | Low — no data loss |
| 401 Unauthorized | Invalid or missing API key header | Regenerate key, fix Authorization header | 5–15 minutes | Low |
| 403 Forbidden | API key lacks required permission scope | Add Mail Send scope in dashboard | 5 minutes | Low |
| 502 Bad Gateway | SendGrid infra issue or wrong endpoint URL | Verify endpoint URL, retry with backoff | Minutes (if transient) | Low |
| Connection Refused | Firewall blocking outbound 443 or wrong host | Whitelist SendGrid IPs, verify hostname | 30–60 minutes | Medium — requires infra change |
| Timeout | Slow client, large payload, or network latency | Increase client timeout to 30s, reduce payload | 1–2 hours | Low |
| Webhook Not Firing | Endpoint returning 4xx/5xx or SSL error | Fix endpoint response codes, renew cert | 1–3 hours | Medium |
Understanding SendGrid Errors
SendGrid's v3 Mail Send API (https://api.sendgrid.com/v3/mail/send) returns standard HTTP status codes. Knowing what each code means in SendGrid's context is the first step to a fast resolution.
| Status Code | SendGrid Meaning |
|---|---|
| 202 Accepted | Message queued successfully |
| 400 Bad Request | Malformed JSON or missing required fields |
| 401 Unauthorized | API key missing or invalid |
| 403 Forbidden | API key lacks required permissions |
| 413 Payload Too Large | Email exceeds 30 MB total |
| 429 Too Many Requests | Rate limit exceeded |
| 500/502/503/504 | SendGrid server-side error |
Step 1: Identify the Exact Error
Always log the full HTTP response — status code, headers, and body. The response body contains SendGrid's error array:
{
"errors": [
{
"message": "The provided authorization grant is invalid, expired, or revoked",
"field": null,
"help": null
}
]
}
For rate limits, SendGrid returns Retry-After and X-RateLimit-Reset headers. Capture these.
Step 2: Fix 429 Rate Limit Exceeded
SendGrid enforces 100 requests per second on the v3 API for most plans. The monthly volume limit depends on your plan tier. When you hit the per-second limit you receive:
HTTP 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708700400
Retry-After: 2
Immediate fixes:
- Read
Retry-After— never hardcode a retry delay. Parse the header value (seconds) and wait that long before retrying. - Batch sends with the
personalizationsarray — instead of 100 individual API calls, send one request with up to 1,000personalizations. This reduces API call volume by 99%. - Implement a token bucket or leaky bucket queue in your application layer to smooth bursts before they hit the API.
- Use a dedicated IP and subuser to segment transactional vs. marketing email, each with its own rate limit budget.
For monthly limits, check GET /v3/stats to monitor consumption before you hit the ceiling.
Step 3: Fix 401 Unauthorized
The exact error message is: Permission denied, wrong credentials
Common causes:
- Missing
Authorizationheader — must beAuthorization: Bearer YOUR_API_KEY(notAuthorization: Basic ...) - Whitespace or newline in the key — a common copy-paste bug; trim the key value
- API key deleted or expired — check the SendGrid dashboard under Settings → API Keys
- Wrong environment — using a sandbox key against production endpoint or vice versa
Verification steps:
curl -i -X GET https://api.sendgrid.com/v3/scopes \
-H "Authorization: Bearer $SENDGRID_API_KEY"
Expected: HTTP 200 with a JSON array of scopes. If you get 401, the key is wrong.
Step 4: Fix 403 Forbidden
You have a valid key but it lacks the required scope. The error body reads:
{"errors":[{"message":"Access forbidden"}]}
Navigation path to fix: SendGrid Dashboard → Settings → API Keys → Edit Key → Mail Send → Full Access
Minimum required scopes for sending email:
mail.send
If using the Inbound Parse webhook or Email Activity Feed, you also need:
webhook.read/webhook.writeemail_activity.read
Step 5: Fix 502 Bad Gateway and Connection Refused
502 errors from SendGrid are usually transient (lasting <5 minutes). Check https://status.sendgrid.com/ for active incidents before spending time debugging.
If no incident is reported:
- Verify your endpoint URL is exactly
https://api.sendgrid.com/v3/mail/send— a trailing slash or typo causes 404/502 - Check your HTTP client is using TLS 1.2 or 1.3. SendGrid dropped TLS 1.0/1.1 support in 2020.
- Test DNS resolution:
dig api.sendgrid.comshould return Twilio SendGrid's CDN IP range
Connection refused usually means outbound TCP/443 is blocked by a firewall or security group. SendGrid publishes its IP ranges; ensure your egress rules allow them.
Step 6: Fix Timeout Errors
SendGrid's API typically responds in <2 seconds. If your client times out:
- Increase client timeout to at least 30 seconds — the default 5s in many HTTP libraries is too short under load
- Check payload size — the API enforces a 30 MB limit. Large attachments increase latency; consider linking to hosted files instead
- Use connection pooling — opening a new TCP connection per request adds 100–300ms of TLS handshake overhead
- Move sends to an async queue — never call SendGrid synchronously from a web request handler; use a background worker
Step 7: Fix Webhook Not Working
SendGrid webhooks (Event Webhook) send POST requests to your endpoint. If events are not arriving:
- Verify your endpoint returns HTTP 2xx within 3 seconds — SendGrid retries up to 3 times with 5-minute spacing, then drops the event
- Check SSL certificate validity — use
curl -I https://yourdomain.com/sendgrid-webhookto verify TLS; self-signed certs will cause SendGrid to refuse delivery unless you disable verification (not recommended for production) - Whitelist SendGrid's webhook IP ranges — documented at https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features
- Enable signed webhooks — use the
X-Twilio-Email-Event-Webhook-Signatureheader to verify authenticity and debug delivery issues - Test with the Event Webhook Tester in the SendGrid dashboard (Settings → Mail Settings → Event Webhook → Test)
Step 8: Validate End-to-End with a Minimal Reproduction
When debugging any SendGrid error, strip the request down to the minimum:
curl -v -X POST https://api.sendgrid.com/v3/mail/send \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"personalizations":[{"to":[{"email":"test@example.com"}]}],
"from":{"email":"verified-sender@yourdomain.com"},
"subject":"Test",
"content":[{"type":"text/plain","value":"Test email"}]
}'
This isolates whether the issue is in your API key, sending domain verification, or application code.
Frequently Asked Questions
#!/usr/bin/env bash
# SendGrid Diagnostic Script
# Usage: SENDGRID_API_KEY=your_key bash sendgrid-diag.sh
set -euo pipefail
API_KEY="${SENDGRID_API_KEY:-}"
BASE_URL="https://api.sendgrid.com"
if [[ -z "$API_KEY" ]]; then
echo "ERROR: Set SENDGRID_API_KEY environment variable" >&2
exit 1
fi
echo "=== 1. DNS Resolution ==="
dig +short api.sendgrid.com | head -5
echo ""
echo "=== 2. TLS Handshake Check ==="
curl -sv --max-time 10 "$BASE_URL" 2>&1 | grep -E '(SSL|TLS|Connected|expire|subject)'
echo ""
echo "=== 3. API Key Validation (GET /v3/scopes) ==="
HTTP_STATUS=$(curl -s -o /tmp/sg_scopes.json -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
"$BASE_URL/v3/scopes")
echo "HTTP Status: $HTTP_STATUS"
if [[ "$HTTP_STATUS" == "200" ]]; then
echo "Granted scopes:"
python3 -c "import json,sys; d=json.load(open('/tmp/sg_scopes.json')); [print(' -', s) for s in d.get('scopes', []) if 'mail' in s or 'webhook' in s]"
else
echo "ERROR Response:"
cat /tmp/sg_scopes.json
fi
echo ""
echo "=== 4. Rate Limit Headers (test request) ==="
curl -si -o /dev/null -D - -X POST "$BASE_URL/v3/mail/send" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"personalizations":[{"to":[{"email":"noop@example.com"}]}],
"from":{"email":"noop@example.com"},"subject":"diag",
"content":[{"type":"text/plain","value":"diag"}]}' 2>&1 \
| grep -E '(X-RateLimit|Retry-After|HTTP/)'
echo ""
echo "=== 5. Current Send Stats (last 24h) ==="
YESTERDAY=$(date -u -d '1 day ago' '+%Y-%m-%d' 2>/dev/null || date -u -v-1d '+%Y-%m-%d')
TODAY=$(date -u '+%Y-%m-%d')
curl -s \
-H "Authorization: Bearer $API_KEY" \
"$BASE_URL/v3/stats?start_date=$YESTERDAY&end_date=$TODAY" \
| python3 -c "
import json,sys
data=json.load(sys.stdin)
for day in data:
stats=day.get('stats',[{}])[0].get('metrics',{})
print(f"Date: {day['date']}")
print(f" Requests: {stats.get('requests',0)}")
print(f" Delivered: {stats.get('delivered',0)}")
print(f" Bounces: {stats.get('bounces',0)}")
print(f" Blocks: {stats.get('blocks',0)}")
"
echo ""
echo "=== 6. Webhook Endpoint Test ==="
if [[ -n "${WEBHOOK_URL:-}" ]]; then
HTTP_CODE=$(curl -s -o /tmp/wh_resp.json -w "%{http_code}" --max-time 5 \
-X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '[{"event":"test","email":"test@example.com","timestamp":1708700400}]')
echo "Webhook endpoint returned: $HTTP_CODE"
[[ "$HTTP_CODE" =~ ^2 ]] && echo 'OK' || echo 'FAIL - must return 2xx within 3s'
else
echo "Set WEBHOOK_URL=https://yourdomain.com/webhook to test endpoint"
fi
echo ""
echo "=== Diagnostics Complete ==="Error Medic Editorial
Error Medic Editorial is a team of senior DevOps and SRE engineers with combined experience across cloud-native infrastructure, API integrations, and production incident response. We specialize in translating complex debugging scenarios into actionable runbooks that engineering teams can execute under pressure.
Sources
- https://docs.sendgrid.com/api-reference/mail-send/mail-send
- https://docs.sendgrid.com/ui/account-and-settings/api-keys
- https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features
- https://docs.sendgrid.com/for-developers/sending-email/rate-limits
- https://status.sendgrid.com/
- https://stackoverflow.com/questions/tagged/sendgrid
- https://github.com/sendgrid/sendgrid-nodejs/issues