Telnyx Logo

Outbound Call Limiter

Smart Call Management with Queue-Based Parallel Processing

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.