The On-Demand Problem
Your tenth customer signs up. They need a phone number. You make an API call to Twilio, search for available numbers, buy one, configure the webhooks, and return it to the customer.
This takes 3-5 seconds. Fine for customer ten.
Your hundredth customer signs up during a Product Hunt launch. Twenty people click "get started" in the same minute. Each one triggers a Twilio number search. API rate limits kick in. Number availability drops. Some searches return zero results. Customers see error messages. Your launch is now a support ticket factory.
On-demand number buying doesn't scale. Warm number pools do.
What Is a Warm Number Pool?
A warm number pool is a collection of pre-purchased phone numbers sitting in your Twilio account, ready to assign to tenants instantly. No API calls to search. No waiting for carrier activation. Just pop from the pool and configure.
The pool has two operations:
- Allocate — assign a number from the pool to a tenant
- Replenish — buy new numbers when the pool drops below a threshold
That's it. The concept is simple. The implementation has a few edge cases.
Why On-Demand Fails
There are four ways on-demand number buying breaks in production:
Rate limits. Twilio's number search API is rate-limited. Heavy burst traffic during launches, promotions, or viral moments will hit the limit. When it does, number provisioning fails. Customer onboarding fails. Revenue is lost.
Availability gaps. Not all area codes have available numbers at all times. If a customer wants a New York number and Twilio's stock is empty, you either return an error or assign a number from a different area code. Neither is a great customer experience.
Carrier delays. Some numbers, especially toll-free or certain geographic regions, can take minutes or hours to fully activate after purchase. If you buy on demand and immediately route calls to the number, some calls will fail.
Cost variability. Twilio charges per number per month whether it's assigned or not. With on-demand buying, you only pay for numbers you need. But if you hit an availability gap and need to buy premium numbers, costs spike unexpectedly.
The Warm Pool Implementation
Here's a simplified warm pool system in TypeScript:
// Buy numbers in the background and keep them warm
async function replenishPool(targetSize: number = 20) {
const current = await db.warmNumber.count({
where: { status: "warm" },
});
const needed = targetSize - current;
if (needed <= 0) return;
const available = await twilio.availablePhoneNumbers("US").local.list({
voiceEnabled: true,
smsEnabled: true,
limit: needed,
});
for (const num of available) {
const purchased = await twilio.incomingPhoneNumbers.create({
phoneNumber: num.phoneNumber,
});
await db.warmNumber.create({
data: {
e164: purchased.phoneNumber,
twilioSid: purchased.sid,
status: "warm",
},
});
}
}
Run this on a schedule (every hour, or triggered by a low-pool alert). Keep the pool at your target size.
Allocating from the pool:
async function allocateNumber(tenantId: string): Promise<string> {
const warmNumber = await db.warmNumber.findFirst({
where: { status: "warm" },
orderBy: { createdAt: "asc" }, // FIFO
});
if (!warmNumber) {
throw new Error("Pool empty — replenish in progress");
}
// Configure the number for this tenant
await twilio.incomingPhoneNumbers(warmNumber.twilioSid).update({
voiceUrl: `${APP_URL}/api/twilio/voice`,
voiceMethod: "POST",
});
// Mark as allocated
await db.warmNumber.update({
where: { id: warmNumber.id },
data: {
status: "allocated",
tenantId,
allocatedAt: new Date(),
},
});
return warmNumber.e164;
}
This is sub-second. The number is already bought, already in your account, just needs webhook configuration.
Health Checks Before Allocation
Not every warm number works. Carriers can delay activation, especially for:
- Toll-free numbers
- Recently released numbers
- Numbers in certain rural area codes
Before allocating, validate:
async function validateNumber(e164: string): Promise<boolean> {
try {
const testCall = await twilio.calls.create({
to: e164,
from: TEST_FROM_NUMBER,
twiml: "<Response><Hangup/></Response>",
timeout: 10,
});
await new Promise((resolve) => setTimeout(resolve, 3000));
const status = await twilio.calls(testCall.sid).fetch();
return status.status !== "failed";
} catch {
return false;
}
}
Run this on all numbers in the pool before marking them as "warm." If validation fails, discard the number and buy a replacement.
Pool Sizing
How big should your warm pool be? Depends on your signup velocity:
| Signups/day | Pool size | Replenish trigger |
|---|---|---|
| < 10 | 10 | When pool < 5 |
| 10-50 | 25 | When pool < 10 |
| 50-200 | 50 | When pool < 20 |
| 200+ | 100+ | When pool < 30 + auto-scaling |
The cost is $1-2 per number per month. A pool of 50 numbers costs ~$75/month. Compare that to the revenue lost from failed onboarding during a launch.
Edge Cases
Pool exhaustion. If signups spike beyond your pool size, you'll still hit delays. Have a fallback: buy on-demand with a longer timeout, and show the customer a "provisioning in progress" message.
Number recycling. When a tenant churns, their number returns to the pool. But the number may have been used in marketing materials, SMS campaigns, or customer contacts. Decide your policy: recycle after a cooling period (30-90 days) or permanently retire.
Geographic preferences. Some customers need specific area codes. Maintain sub-pools by region: NYC pool, LA pool, toll-free pool. This complicates replenishment but improves customer experience.
The Bottom Line
Warm number pools aren't glamorous. They're infrastructure. But they're the difference between a customer getting a working number in 500ms and a customer getting an error during your launch.
If you're building voice-agent SaaS, implement the pool early. It's easier to do when you have ten customers than when you have a thousand and the system is on fire.
Callforge handles warm number pools, health checks, and automatic replenishment out of the box. If you don't want to build this yourself: callforge.dev
Have you hit number availability issues with Twilio? How did you solve it?
Top comments (0)