What This System Does
The Outbound Call Limiter automatically manages how many times your clubs can call members and guests each day. You control the limits by uploading a simple spreadsheet, and the system handles everything else - processing millions of records in seconds using advanced parallel processing.
✅ Key Benefits:
- Lightning Fast: Process 1M+ records in 30-60 seconds using parallel queue processing
- Prevent Over-Calling: Set daily limits per phone number (1, 3, 10, or any number you choose)
- Compliance: Automatically blocks calls when limits are reached
- Flexibility: Update limits anytime by uploading a new CSV file
- Scale: Manage up to 3 million phone numbers efficiently
- Remove Numbers: In a delta upload, set limit to 0 to remove a number from the system entirely
- Allow Unlimited: Numbers not in your list can be called freely
📋 How to Use
Simple 3-Step Process
1
Create Your Spreadsheet
Make a CSV file with two columns: Phone numbers and daily call limits
2
Upload to System
Use the upload page to import your data (takes 30-60 seconds even for 1M+ records)
3
That's It!
The system automatically enforces limits on all outbound calls from your clubs
💡 What Happens When Someone Calls:
If the number IS in your CSV:
• System checks how many times it's been called today
• If under the limit → Call goes through normally
• If at the limit → Caller hears: "Sorry, you've reached the maximum number of times you can call this number. Please try again tomorrow."
If the number is NOT in your CSV:
• Call goes through with no restrictions (unlimited calls allowed)
Uploading Your Data
File Format
We're expecting a CSV (comma-separated values) file with two columns:
📋 Required Columns:
1. Phone - The phone number (10 digits, no dashes, spaces, or +1)
2. Limit - The daily call limit for that number (any whole number)
Special value: In a delta upload, setting limit to 0 will remove that phone number from the system entirely. The number will then have no restrictions (unlimited calls allowed).
⚠️ Important Rules:
• Phone numbers must be 10 digits (no dashes, spaces, or +1)
• Limits can be any whole number (1 for guests, 3 for members, or any other value)
• In a delta upload, setting limit to 0 removes the number from the system (allowing unlimited calls)
• In a full upload, limit 0 is stored as-is (blocks all calls to that number)
• First row must be headers: Phone,Limit
Two Upload Types
| Upload Type |
When to Use |
What It Does |
| Full (Replace All) |
Every morning with your complete list |
Replaces ALL numbers in the system. Previous data is erased. Use this for your main daily update. |
| Delta (Add/Update/Remove) |
Throughout the day for changes |
Adds or updates specific numbers. Set limit to 0 to remove a number. Perfect for mid-day adjustments. |
Upload Methods
✅ Web Upload (Recommended for Manual Use)
Use the web interface for manual uploads, testing, or one-off updates.
Open Upload Page →
Features drag-and-drop file upload and visual progress indicators.
💻 API Upload (For Automation)
Best for scheduled daily uploads and automation. Use cURL, Python, or any HTTP client to upload programmatically.
Full Upload (Replace All):
curl -X POST "https://outbound-fitness-international.telnyx-worker.dev/outbound/upload?type=full" -F "file=@/path/to/members.csv"
Delta Upload (Add/Update/Remove):
curl -X POST "https://outbound-fitness-international.telnyx-worker.dev/outbound/upload?type=delta" -F "file=@/path/to/updates.csv"
Perfect for: Cron jobs, scheduled tasks, CI/CD pipelines, automated workflows
Processing Times:
• Small files (under 1000 rows): 5-10 seconds
• Medium files (10,000-100,000 rows): 10-20 seconds
• Large files (1-3 million rows): 30-60 seconds
How It's So Fast: The system splits your CSV into 100,000-row chunks and processes them in parallel using Cloudflare Queues. A 1.16M record file becomes 12 chunks that all process simultaneously!
⚙️ How It Works (Technical Overview)
This section is for technical reference. As a customer, you don't need to worry about these details - the system handles everything automatically!
Upload Architecture: Queue-Based Parallel Processing
Why It's Fast & Resource-Efficient:
Instead of processing your CSV file sequentially, we use parallel processing via Cloudflare Queues:
1. Upload & Store - CSV is uploaded to R2 storage and a split job is queued
2. Smart Chunking - The split worker breaks the CSV into 100,000-row chunks, stored in R2
3. Parallel Queue Processing - All chunks are sent to Cloudflare Queues and processed simultaneously by multiple workers
4. Atomic Progress Tracking - Each chunk atomically increments a D1 counter. The last chunk to finish triggers completion
5. Safe Table Switching - For full uploads, the old table stays active until the new one is fully populated, then switches instantly
Example: A 1.16M record file becomes 12 chunks that all process at the same time, completing in 30-60 seconds.
What Happens During Upload
1
File Received
Main worker receives your CSV, stores it in R2, and sends a split job to the queue.
2
Split into Chunks
The split worker breaks the CSV into 100,000-row chunks, stores each in R2, and queues them for processing.
For full uploads, a new versioned table is created. For delta uploads, the existing active table is used.
3
Parallel Processing
Queue consumers (up to 10 concurrent) process chunks simultaneously. Each chunk writes records to the database in batches. For delta uploads, rows with limit = 0 are deleted instead of inserted.
4
Completion
Each chunk atomically increments a D1 counter. When the last chunk finishes:
- Full upload: Switches the active table pointer to the new table, drops the old one
- Delta upload: Table was already active — just marks upload as done
Technical Constraints & Solutions
| Constraint |
Limit |
Our Solution |
| Worker CPU Time |
30 seconds (paid) |
Store CSV in R2 and queue a split job — main worker finishes fast ✓ |
| Large CSV Files |
Memory limits |
Split into 100K-row chunks stored in R2, processed independently ✓ |
| Concurrent Processing |
Race conditions |
Atomic D1 counter tracks chunk completion — no KV race conditions ✓ |
| Zero Downtime (Full) |
Table swap during writes |
Old table stays active until new table is fully populated, then switches ✓ |
System Reliability
✅ Built for Scale:
• Parallel processing: Up to 10 queue consumers running simultaneously
• Automatic retries: Failed chunks retry up to 3 times before being marked as "failed"
• Zero downtime: Full uploads keep the old table active until the new one is complete
• Atomic progress: D1-based counter ensures accurate chunk completion tracking
• 99.9% uptime: Runs on Cloudflare's global network
• Real-time status: Check upload progress via the statistics page or API
Monitoring Your Upload:
When you upload a file, you receive an upload_history_id. Use this to check status:
GET /outbound/upload/status?id={upload_history_id}
Returns:
• status: "processing" (in progress), "completed" (done), or "failed" (error after 3 retries)
• records_processed: Total rows handled
• records_added: How many records were inserted/updated
• records_removed: How many records were deleted (delta only, limit = 0)
• chunks_completed / total_chunks: Processing progress
Full vs Delta Upload Behavior
Full Upload (Replace All):
What happens:
• Creates a new versioned table while the old table continues serving queries
• All chunks insert records into the new table
• Last chunk: Switches the active pointer to the new table, drops the old one, marks upload as "completed"
Use case: Morning refresh — complete master list replacement with zero downtime
Delta Upload (Add/Update/Remove):
What happens:
• Modifies the current active table in-place
• limit > 0: Add or update the phone number
• limit = 0: Remove the phone number from the system
Use case: Mid-day adjustments — add new numbers, change limits, or remove numbers
API Endpoints
Upload CSV
POST /outbound/upload?type={full|delta}
Parameters:
• type: "full" (full replace) or "delta" (add/update/remove)
• "daily" is accepted as an alias for "full" (backward compatible)
Body:
• Content-Type: multipart/form-data
• Field: "file" (CSV file)
Response (200 OK):
{
"message": "CSV upload successful, processing started",
"uploadId": "550e8400-e29b-41d4-a716-446655440000",
"totalRows": 1160103,
"estimatedChunks": 12
}
Check Call Limit
GET /outbound/check?phone={phone_number}
Response (allowed):
{
"phone": "5551234567",
"allowed": true,
"call_limit": 3,
"current_count": 1,
"remaining": 2
}
Response (blocked):
{
"phone": "5551234567",
"allowed": false,
"call_limit": 3,
"current_count": 3,
"remaining": 0
}
Note: Counter automatically resets at midnight EST (first call after midnight)
Check Upload Status
GET /outbound/upload/status?id={upload_history_id}
Response:
{
"id": 15,
"status": "completed", // or "processing"
"upload_type": "full",
"filename": "members.csv",
"total_records": 1160103,
"records_processed": 1160103,
"records_added": 1160103,
"records_removed": 0,
"total_chunks": 12,
"chunks_completed": 12,
"uploaded_at": "2025-01-07T10:30:00Z",
"completed_at": "2025-01-07T10:31:15Z"
}
Get Upload History
GET /outbound/upload/history
Response:
{
"success": true,
"count": 10,
"uploads": [
{
"id": 15,
"upload_type": "full",
"filename": "members.csv",
"records_processed": 1160103,
"uploaded_at": "2025-01-07T10:30:00Z"
},
...
]
}
Get Statistics
GET /outbound/stats
Response:
{
"total_numbers": 1160103,
"at_limit": 42,
"average_limit": 2
}
Reset Call Counters
POST /outbound/reset
Option 1: Reset ALL numbers
{
"reset_all": true
}
Response:
{
"success": true,
"message": "All counts reset to 0",
"reset_date": "2025-11-07",
"numbers_reset": 1160103
}
Option 2: Reset specific numbers (array)
{
"phones": [
"555-123-4567",
"+1 (555) 987-6543",
"5551234567"
]
}
Response:
{
"success": true,
"message": "Reset 3 phone number(s)",
"reset_date": "2025-11-07",
"requested": 3,
"normalized": 3,
"numbers_reset": 3
}
Notes:
• Counters automatically reset at midnight EST
• Phone numbers are normalized (handles various formats)
• Use either reset_all OR phones array (not both)
Delete Phone Number
DELETE /outbound/delete
Headers:
• Authorization: Bearer {token}
Body:
{
"phone": "5551234567"
}
Response:
{
"success": true,
"message": "Phone number deleted from 1 table(s)",
"phone": "5551234567",
"records_deleted": 1,
"tables_affected": ["outbound_numbers_0310_1"]
}
Note: Removes the phone number from all active tables.
For bulk removals, use a delta upload with limit = 0 instead.