Hardening Third-Party API Calls: Twilio & Stripe Timeouts
Hardening Third-Party API Calls: Twilio & Stripe Timeouts
Release: v0.1.111 · Category: Error Resilience
Overview
Production systems that depend on external APIs need to enforce a clear contract: if the remote service doesn't respond within a reasonable window, the call fails fast so the rest of the system stays healthy. This release closes a gap where two critical third-party integrations — Twilio (SMS notifications) and Stripe (billing) — had no timeout boundaries.
The Problem
Before this fix, the platform had an inconsistency in how it handled outbound HTTP calls:
| Integration | Timeout Applied? |
|---|---|
| OFSI consolidated list sync | ✅ AbortSignal.timeout() |
| UN/EU/OFAC data fetches | ✅ AbortSignal.timeout() |
Twilio SMS (src/lib/sms.ts) | ❌ None |
Stripe SDK (lib/stripe.ts) | ❌ None |
A raw fetch() call with no AbortSignal will wait indefinitely if the remote server accepts the TCP connection but never sends a response. On a compliance platform where time-sensitive alerts (sanctions match notifications) and billing operations run synchronously, an indefinitely hung call creates two risks:
- Notification delays — A stuck Twilio call means a compliance officer may never receive a time-critical sanctions alert SMS.
- Blocked billing flows — A stuck Stripe call holds up subscription or payment operations with no recovery path.
The Fix
Twilio — src/lib/sms.ts
The fetch call to the Twilio API now includes an AbortSignal.timeout() option:
// Before
const response = await fetch(twilioUrl, {
method: 'POST',
headers: { ... },
body: formData,
});
// After
const response = await fetch(twilioUrl, {
method: 'POST',
headers: { ... },
body: formData,
signal: AbortSignal.timeout(10000), // 10-second hard limit
});
AbortSignal.timeout(10000) is a built-in Web API that creates an abort signal that automatically fires after the specified number of milliseconds. If Twilio does not complete the response within 10 seconds, the fetch is aborted and a TimeoutError (or DOMException with name TimeoutError) is thrown, which can be caught and handled by your error handling layer.
Stripe — lib/stripe.ts
The Stripe Node.js SDK accepts a timeout option directly in its constructor. This timeout applies to all requests made through that client instance:
// Before
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// After
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
timeout: 10000, // 10-second hard limit on all Stripe API calls
});
This 10-second limit now applies to every Stripe SDK operation — charge creation, subscription updates, webhook verification, and anything else routed through this client.
Timeout Value Rationale
Both timeouts are set to 10,000 ms (10 seconds). This aligns with the timeouts already applied to OFSI/UN/EU/OFAC data fetches elsewhere in the platform, providing a consistent baseline across all external calls. For a REST API call that should complete in well under a second under normal conditions, 10 seconds provides ample headroom for transient latency without allowing indefinite hangs.
What You Should Know
- No behaviour change under normal conditions. Twilio and Stripe calls that complete within 10 seconds are unaffected.
- Timeout errors are surfaced, not swallowed. When a timeout fires, the error propagates to your existing error handling. Ensure your notification and billing flows handle timeout errors appropriately (e.g., logging, retrying, or surfacing an error to the user).
- This does not add retry logic. Timeouts and retries are separate concerns. This change ensures failures are fast; retry strategies should be layered on top where appropriate.