📞 Outbound Call Limiter

Support Team Troubleshooting Guide

Production: https://outbound-fitness-international.solutions-2bd.workers.dev

1. System Overview

The Outbound Call Limiter is a Cloudflare Workers-based system that enforces per-phone-number daily call limits. It integrates with Telnyx telephony to manage outbound calls from fitness clubs.

Key Capabilities

  • ✅ Process CSV files with phone numbers and daily call limits
  • ✅ Check if a phone number can be called based on current day's usage
  • ✅ Automatically reset call counts at midnight EST
  • ✅ Handle Telnyx webhooks for call control
  • ✅ Support zero-downtime CSV uploads using versioned tables
💡 Important: The system uses EST timezone (UTC-5) for daily resets. Midnight EST is when all counters automatically reset.

2. Architecture & Components

Cloudflare Resources

Resource Name Purpose
Worker outbound-fitness-international Main application logic
D1 Database outbound_limiter_db Stores phone numbers, limits, and call logs
D1 Database inbound-fitness-international Looks up club phone numbers
R2 Bucket fitness-assets Stores CSV uploads and static assets
KV Namespace PROGRESS_KV Tracks CSV upload progress
Queue csv-split-queue Splits large CSV files into chunks
Queue csv-processing-queue Processes CSV chunks in parallel

Database Schema

Main Tables

  • outbound_numbers_MMDD_N - Versioned phone number tables (e.g., outbound_numbers_1211_1)
  • active_table - JSON array tracking which versioned tables are active
  • call_logs - History of all call attempts (allowed and blocked)
  • upload_history - Tracks all CSV uploads and their status
⚠️ Versioned Tables: Each CSV upload creates a new table (e.g., outbound_numbers_1211_1). The system keeps the 2 most recent tables for zero-downtime updates.

Data Flow

1. CSV Upload → User uploads CSV via web UI or API
2. Split Queue → File is parsed and split into 100K-row chunks
3. Process Queue → Chunks processed in parallel, data written to new versioned table
4. Table Switch → New table added to active_table array, old tables cleaned up
5. Call Check → System queries active tables for phone limits

3. API Endpoints Reference

POST /outbound/upload?type={daily|delta}

Purpose: Upload CSV file with phone numbers and call limits

Auth: Bearer token required (UPLOAD_BEARER_TOKEN)

Body: multipart/form-data with "file" field

Parameters

  • type=daily - Creates new versioned table (recommended for daily updates)
  • type=delta - Creates new versioned table (same behavior as daily)

Example Request

curl -X POST "https://outbound-fitness-international.solutions-2bd.workers.dev/outbound/upload?type=daily" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "file=@members.csv"

Success Response (200)

{ "message": "CSV upload successful, processing started", "uploadId": "550e8400-e29b-41d4-a716-446655440000", "totalRows": 1160103, "estimatedChunks": 12 }

🔧 Troubleshooting Steps

  1. 401 Unauthorized: Verify bearer token is correct. Check env.UPLOAD_BEARER_TOKEN in wrangler.toml
  2. 400 Bad Request - "Failed to extract CSV":
    • Ensure Content-Type is multipart/form-data
    • Verify field name is "file"
    • Check file is valid CSV format
  3. File not processing: Check queue status:
    wrangler tail --config outbound-wrangler.toml --format pretty
  4. CSV format issues: Verify headers are "phone" and "limit" (case-insensitive)
GET /outbound/check?phone={number}&from={caller}

Purpose: Check if a phone number can be called today

Auth: None (public endpoint)

Parameters

  • REQUIRED phone - Phone number to check (10 digits)
  • OPTIONAL from - Calling number (for logging)

Example Request

curl "https://outbound-fitness-international.solutions-2bd.workers.dev/outbound/check?phone=5551234567&from=5559876543"

Success Response - Call Allowed (200)

{ "phone": "5551234567", "allowed": true, "call_limit": 3, "current_count": 1, "remaining": 2 }

Success Response - Call Blocked (200)

{ "phone": "5551234567", "allowed": false, "call_limit": 3, "current_count": 3, "remaining": 0 }

Success Response - Number Not in Database (200)

{ "phone": "5551234567", "allowed": true, "reason": "Phone number not in database - unlimited calls allowed", "call_limit": null, "current_count": 0, "remaining": null }

🔧 Troubleshooting Steps

  1. 400 Bad Request - "Missing phone parameter": Ensure ?phone= is in URL
  2. 400 Bad Request - "Invalid phone number format":
    • Phone must be 10 or 11 digits
    • System accepts: 5551234567, 15551234567, +15551234567, (555)123-4567
    • Normalized to 10 digits internally
  3. All numbers showing "not in database":
    • Check if CSV upload completed successfully
    • Query active_table to see which tables are active:
      wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT * FROM active_table"
    • Verify table has data:
      wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT COUNT(*) FROM outbound_numbers_1211_1"
  4. Counter not resetting at midnight:
    • System uses EST timezone (UTC-5)
    • Check call_logs table for today's date:
      wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT date(called_at) as date, COUNT(*) FROM call_logs GROUP BY date(called_at) ORDER BY date DESC LIMIT 5"
GET /outbound/stats

Purpose: Get system statistics

Auth: None

Success Response (200)

{ "total_numbers": 1160103, "at_limit": 42, "average_limit": 2, "active_table": "outbound_numbers_1211_1" }

🔧 Troubleshooting Steps

  1. 500 Error - "Unable to query active tables": Active table doesn't exist or is corrupt. Check active_table metadata
  2. total_numbers is 0: No data loaded. Verify CSV upload completed
DELETE /outbound/delete

Purpose: Remove a phone number from active tables

Auth: Bearer token required

Body: JSON with phone number

Example Request

curl -X DELETE "https://outbound-fitness-international.solutions-2bd.workers.dev/outbound/delete" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"phone": "5551234567"}'

Success Response (200)

{ "success": true, "message": "Phone number deleted from 2 table(s)", "phone": "5551234567", "records_deleted": 2, "tables_affected": ["outbound_numbers_1211_1", "outbound_numbers_1211_2"] }

🔧 Troubleshooting Steps

  1. 404 Not Found: Phone number not in any active table
  2. 401 Unauthorized: Missing or invalid bearer token
POST /telnyx/webhook

Purpose: Receive webhooks from Telnyx for call events

Auth: None (Telnyx sends unsigned webhooks)

Handled Events

  • call.initiated (state=parked) - New inbound call, check limits and place outbound call
  • call.answered - Outbound call answered, bridge to parked call or speak rejection message
  • call.hangup - Call ended, handle failed calls by speaking message to parked caller
  • call.speak.ended - Message finished, hang up the call

🔧 Troubleshooting Steps

  1. Calls not being placed:
    • Check worker logs for webhook payloads:
      wrangler tail --config outbound-wrangler.toml --format pretty
    • Verify TELNYX_CONNECTION_ID is correct in wrangler.toml
    • Verify TELNYX_API_KEY secret is set (not visible in logs)
  2. Calls not bridging:
    • Check parked_call_id parameter in webhook URL
    • Verify call.answered webhook includes parked_call_id query param
  3. Wrong rejection message:
    • Check query parameters: limit_reached=true, rate_limit=true, unavailable=true
    • Each triggers different speak message function
  4. Club lookup failing:
    • Check FITNESS_DB has clubs table with forwarding_number and phone columns
    • Query: SELECT phone FROM clubs WHERE forwarding_number = ?

4. Core Functions Breakdown

Main Entry Points

fetch(request, env, ctx)

Purpose: Main HTTP request handler

Routes:

  • GET / → serveHomeUI()
  • GET /outbound → serveUploadUI()
  • POST /outbound/upload → handleCSVUpload()
  • GET /outbound/check → checkLimit()
  • GET /outbound/stats → getStats()
  • DELETE /outbound/delete → handleDeletePhone()
  • POST /telnyx/webhook → handleTelnyxWebhook()
💡 All routes include CORS headers to allow cross-origin requests from web UIs

queue(batch, env)

Purpose: Handle messages from Cloudflare Queues

Queues:

  • csv-split-queue → handleSplitQueue()
  • csv-processing-queue → handleProcessQueue()
⚠️ Symptom: CSV uploaded but no data appearing in database
✅ Solution: Check queue processing logs. Queue consumers may be stuck or failing. Check message retry counts:
wrangler queues consumer list csv-processing-queue --config outbound-wrangler.toml

scheduled(event, env, ctx)

Purpose: Daily cleanup task (runs at midnight UTC via cron)

What it does:

  • Deletes all files in R2 bucket uploads/ directory
  • CSV chunks only needed during processing, safe to delete after

CSV Upload Functions

handleCSVUpload(request, env, corsHeaders)

Purpose: Receives CSV file and initiates processing pipeline

Steps:

  1. Parse multipart/form-data to extract CSV file
  2. Generate unique uploadId (UUID)
  3. Store CSV in R2: uploads/{uploadId}/original.csv
  4. Create upload_history record with status="processing"
  5. Send message to csv-split-queue
⚠️ Common Issue: Multipart parsing failure. The function has fallback logic to manually parse Content-Disposition headers if formData() fails.

handleSplitQueue(batch, env)

Purpose: Split large CSV into 100K-row chunks for parallel processing

Steps:

  1. Fetch CSV from R2 (uploads/{uploadId}/original.csv)
  2. Parse CSV and split into chunks of 100K rows each
  3. Generate next available table name (e.g., outbound_numbers_1211_1)
  4. Create new empty versioned table
  5. Store each chunk in R2: uploads/{uploadId}/chunk-{N}.csv
  6. Send chunk messages to csv-processing-queue

🔧 Troubleshooting Split Queue

  1. File not found in R2: Check ASSETS bucket has uploads/ prefix permissions
  2. Table already exists error: Function retries with next increment number automatically
  3. CSV parsing errors: Check for:
    • Missing headers (must have "phone" and "limit")
    • Invalid CSV structure (quotes, commas in data)

handleProcessQueue(batch, env)

Purpose: Process individual CSV chunks in parallel

Steps:

  1. Check if chunk already processed (prevents duplicates)
  2. Fetch chunk from R2: uploads/{uploadId}/chunk-{N}.csv
  3. For FIRST chunk only: Add new table to active_table array immediately
  4. Parse chunk and normalize phone numbers
  5. Batch insert records into versioned table (INSERT OR REPLACE)
  6. Update upload_history progress counters
  7. Mark chunk as processed in KV
  8. Delete chunk file from R2
  9. For LAST chunk: Mark upload as "completed"
💡 Zero-Downtime Design: First chunk immediately adds new table to active_table array, so queries can start using it while remaining chunks are still processing.

🔧 Troubleshooting Process Queue

  1. Chunks stuck in retry loop:
    • Check worker logs for SQL errors
    • Verify table exists: SELECT name FROM sqlite_master WHERE type='table'
    • Check D1 rate limits (50 writes/sec per database)
  2. Upload never completes:
    • Check chunk_progress table or KV for processed chunks
    • Compare processed count vs totalChunks
    • Look for dead letter queue messages
  3. Duplicate data:
    • Check KV processed:{uploadId}:{chunkIndex} keys
    • Verify INSERT OR REPLACE is being used (not INSERT)

Call Limit Functions

checkLimit(db, url, headers)

Purpose: Determine if a phone number can be called today

Steps:

  1. Normalize phone number (remove formatting, ensure 10 digits)
  2. Query active tables (2 most recent) for phone number
  3. If NOT found: Log as allowed, return allowed=true (unlimited)
  4. If found: Count today's allowed calls from call_logs (EST timezone)
  5. Compare count vs call_limit
  6. Log decision to call_logs (allowed=1 or 0)
  7. Return decision with remaining quota
⚠️ Timezone Behavior: System uses EST (UTC-5) for "today". The query date(called_at) = date('today') uses worker's local time, which is converted to EST using offset calculation.

🔧 Troubleshooting checkLimit

  1. All numbers returning allowed=true:
    • Phone numbers not in database (check active tables)
    • Normalization mismatch (check phone format in database vs request)
  2. Counter not accurate:
    • Check call_logs for duplicate entries
    • Verify date filtering is working:
      SELECT date(called_at), COUNT(*) FROM call_logs WHERE to_number = '5551234567' GROUP BY date(called_at)
  3. Counter not resetting:
    • Verify EST timezone calculation (check current UTC time vs EST time)
    • Check date() function is using correct timezone

Telnyx Integration Functions

handleTelnyxWebhook(env, request, url, headers)

Purpose: Process Telnyx call events and control calls

Main Flow for call.initiated (parked):

  1. Extract parkedCallId, destinationNumber, callerNumber from webhook
  2. Look up club's real phone in FITNESS_DB using forwarding_number
  3. Normalize destination number and check call limit
  4. If limit reached: Reject call (speak limit message)
  5. If allowed: Place outbound call via makeTelnyxCall()
  6. Log decision to call_logs

Main Flow for call.answered:

  1. Check query params: rate_limit, limit_reached, unavailable
  2. If rejection param present: Speak appropriate message
  3. If parked_call_id present: Bridge calls together

Main Flow for call.hangup:

  1. Check if outbound call failed (hangup_cause != normal_clearing)
  2. If failed and parked_call_id present: Answer parked call and speak unavailable message

🔧 Troubleshooting Telnyx Webhooks

  1. Webhooks not received:
    • Check Telnyx dashboard webhook URL is correct
    • Test webhook manually: POST to /telnyx/webhook with sample payload
    • Check firewall/security settings (Telnyx IPs not blocked)
  2. Calls not bridging:
    • Verify parked_call_id is passed in webhook URL
    • Check bridgeTelnyxCalls() logs for API errors
    • Verify both call_control_ids are valid
  3. Rejection messages not playing:
    • Check query params in webhook callback URL
    • Verify speakRejectionMessage/speakRateLimitMessage/speakUnavailableMessage functions
    • Check Telnyx API key has speak permissions
  4. Club lookup failing:
    • Check FITNESS_DB connection binding
    • Verify clubs table structure:
      wrangler d1 execute inbound-fitness-international --config outbound-wrangler.toml \ --command "SELECT * FROM clubs LIMIT 5"

makeTelnyxCall(apiKey, connectionId, toNumber, parkedCallId, fromNumber, workerUrl)

Purpose: Place outbound call via Telnyx API

Returns: Call control data including call_control_id

Error Handling: Sets isRateLimitError flag if status=429

bridgeTelnyxCalls(apiKey, callControlId, targetCallControlId)

Purpose: Connect two calls together

API: POST /v2/calls/{callControlId}/actions/bridge

rejectTelnyxCall(apiKey, callControlId, reason)

Purpose: Answer call and speak limit reached message

Webhook: Includes ?limit_reached=true to trigger speakRejectionMessage()

speakRejectionMessage(apiKey, callControlId)

Message: "Sorry, you've reached the maximum number of times you can call this number today. Please try again tomorrow."

speakRateLimitMessage(apiKey, callControlId)

Message: "We're experiencing high call volume. Please try your call again in a few moments."

speakUnavailableMessage(apiKey, callControlId)

Message: "The called party is temporarily unavailable. Please try again later."

Utility Functions

normalizePhone(phone)

Purpose: Convert any phone format to 10 digits

Accepts: 5551234567, 15551234567, +15551234567, (555)123-4567

Returns: 5551234567 (10 digits) or null if invalid

validateBearerToken(request, expectedToken)

Purpose: Verify Authorization header has correct bearer token

Header format: Authorization: Bearer YOUR_TOKEN

getActiveTableNames(db)

Purpose: Retrieve JSON array of active table names from active_table

Returns: Array like ["outbound_numbers_1211_1", "outbound_numbers_1211_2"]

switchActiveTable(db, newTableName)

Purpose: Add new table to active_table array and cleanup old tables

Logic:

  1. Get current active tables
  2. Drop tables from different days (different MMDD)
  3. Keep max 2 tables from same day
  4. Update active_table with new JSON array

getVersionedTableName(increment)

Purpose: Generate table name like outbound_numbers_1211_1

Format: outbound_numbers_{MMDD}_{increment}

getNextTableIncrement(db)

Purpose: Find next available increment number for today

Example: If outbound_numbers_1211_1 and outbound_numbers_1211_2 exist, returns 3

5. Common Issues & Solutions

Issue 1: CSV Upload Not Processing

Symptom: Customer uploaded CSV but data not appearing, upload stuck at "processing"
Diagnosis Steps:
  1. Check upload_history table for status:
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT * FROM upload_history ORDER BY uploaded_at DESC LIMIT 5"
  2. If status="processing" for >5 minutes, check queue messages:
    wrangler tail --config outbound-wrangler.toml --format pretty
  3. Check R2 for CSV files:
    wrangler r2 object list fitness-assets --config outbound-wrangler.toml --prefix uploads/
  4. Check KV for progress:
    wrangler kv:key list --namespace-id=326282fe83d546699bcca792a343de59
Common Causes:
  • Queue consumer not running (check wrangler.toml has queue bindings)
  • D1 database offline or rate limited
  • CSV parsing error (invalid format, encoding issues)
  • Worker hitting CPU time limit (>30 seconds)
Solutions:
  • Retry upload with smaller file (test with 1000 rows)
  • Check worker logs for specific error messages
  • Verify queue consumers are deployed:
    wrangler deploy --config outbound-wrangler.toml

Issue 2: Phone Numbers Not Being Found

Symptom: /outbound/check returns "not in database" for numbers that should exist
Diagnosis Steps:
  1. Check which tables are active:
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT * FROM active_table WHERE id=1"
  2. Check if phone exists in active table:
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT * FROM outbound_numbers_1211_1 WHERE phone='5551234567'"
  3. Check phone normalization:
    • Customer sends: (555) 123-4567
    • System normalizes to: 5551234567
    • Database may have: +15551234567
Common Causes:
  • Phone format mismatch (database has +1 prefix, query doesn't)
  • CSV upload not completed
  • active_table points to wrong table
Solutions:
  • Verify normalizePhone() function handles all formats
  • Re-upload CSV with correct phone format (10 digits, no +1)
  • Manually fix active_table if needed:
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "UPDATE active_table SET table_name='[\"outbound_numbers_1211_1\"]' WHERE id=1"

Issue 3: Call Counters Not Resetting at Midnight

Symptom: Customer reports calls blocked even though it's a new day
Diagnosis Steps:
  1. Check current time vs EST time:
    // Current UTC time date -u // EST time (UTC-5) TZ='America/New_York' date
  2. Check call_logs to see when counts are dated:
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT to_number, date(called_at) as call_date, COUNT(*) FROM call_logs WHERE to_number='5551234567' GROUP BY date(called_at) ORDER BY call_date DESC"
  3. Verify timezone offset calculation in checkLimit() function
Common Causes:
  • Timezone calculation error (EST offset not applied correctly)
  • date() function using UTC instead of EST
  • Call logs not being recorded with correct timestamp
Solutions:
  • Verify EST offset: -5 hours from UTC (or -4 during DST)
  • Check date filtering query in checkLimit():
    WHERE date(called_at) = date('today')
    Should use EST-adjusted date

Issue 4: Calls Not Bridging (No Audio)

Symptom: Telnyx calls connect but parties can't hear each other
Diagnosis Steps:
  1. Check worker logs for bridge API calls:
    wrangler tail --config outbound-wrangler.toml --format pretty | grep bridge
  2. Verify call.answered webhook includes parked_call_id parameter
  3. Check Telnyx dashboard for call legs and bridge status
Common Causes:
  • parked_call_id not passed in webhook URL
  • Bridge API call failing (invalid call_control_id)
  • One call leg hung up before bridge completed
Solutions:
  • Verify webhook URL format in makeTelnyxCall():
    const webhookUrl = `${workerUrl}/telnyx/webhook?parked_call_id=${parkedCallId}`;
  • Check both call_control_ids are valid and active
  • Add more logging to bridgeTelnyxCalls() function

Issue 5: Wrong Rejection Message

Symptom: Caller hears wrong message (e.g., "unavailable" instead of "limit reached")
Diagnosis Steps:
  1. Check webhook query parameters in logs
  2. Verify which speak function is being called
  3. Check call.answered event handler logic
Query Parameter Logic:
  • ?rate_limit=true → "High call volume" message
  • ?limit_reached=true → "Maximum number of calls" message
  • ?unavailable=true → "Temporarily unavailable" message
Solutions:
  • Verify rejectTelnyxCall() sets correct webhook URL
  • Check if/else logic in call.answered handler
  • Test each message type manually

Issue 6: High D1 Database Latency

Symptom: Slow responses from /outbound/check, queries timing out
Diagnosis Steps:
  1. Check D1 metrics in Cloudflare dashboard
  2. Look for slow queries in logs
  3. Check table sizes:
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT name, COUNT(*) FROM sqlite_master sm JOIN pragma_table_info(sm.name) WHERE type='table' GROUP BY name"
Common Causes:
  • Large call_logs table (millions of rows)
  • Missing indexes on frequently queried columns
  • Multiple active tables being queried sequentially
Solutions:
  • Add index on call_logs(to_number, called_at):
    wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "CREATE INDEX IF NOT EXISTS idx_call_logs_number_date ON call_logs(to_number, called_at)"
  • Archive old call_logs data (move to R2 or separate table)
  • Consider pagination for stats queries

6. Monitoring & Debugging

View Live Logs

wrangler tail --config outbound-wrangler.toml --format pretty

This shows real-time logs from the worker including:

  • HTTP requests and responses
  • Queue messages being processed
  • Database queries
  • Errors and exceptions

Query Database Directly

Check Active Tables

wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT * FROM active_table"

Count Phone Numbers

wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT COUNT(*) as total FROM outbound_numbers_1211_1"

Check Recent Call Logs

wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT * FROM call_logs ORDER BY called_at DESC LIMIT 20"

Find Phone Number Across All Tables

wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'outbound_numbers_%'"

Check Upload History

wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "SELECT id, upload_type, filename, status, records_processed, uploaded_at FROM upload_history ORDER BY uploaded_at DESC LIMIT 10"

Check Queue Status

List Queue Consumers

wrangler queues consumer list csv-processing-queue --config outbound-wrangler.toml

View Queue Messages (if accessible)

# Note: Direct queue message viewing requires special permissions # Check worker logs instead for queue processing

Check R2 Storage

List CSV Uploads

wrangler r2 object list fitness-assets --config outbound-wrangler.toml --prefix uploads/

Download CSV File

wrangler r2 object get fitness-assets --config outbound-wrangler.toml uploads/UPLOAD_ID/original.csv --file downloaded.csv

Check KV Store

List All Keys

wrangler kv:key list --namespace-id=326282fe83d546699bcca792a343de59

Get Upload Progress

wrangler kv:key get "progress:UPLOAD_ID" --namespace-id=326282fe83d546699bcca792a343de59

Cloudflare Dashboard

Monitor worker metrics in Cloudflare dashboard:

  • Workers & Pages → outbound-fitness-international → Metrics
  • View: Requests per second, error rate, CPU time, success rate
  • D1 → outbound_limiter_db → Metrics
  • View: Query latency, queries per second, storage usage

Health Check Endpoints

Test system health with these quick checks:

1. Worker Responding

curl https://outbound-fitness-international.solutions-2bd.workers.dev/

Should return HTML home page

2. Database Connected

curl https://outbound-fitness-international.solutions-2bd.workers.dev/outbound/stats

Should return JSON with total_numbers, at_limit, etc.

3. Check Limit Working

curl "https://outbound-fitness-international.solutions-2bd.workers.dev/outbound/check?phone=5551234567"

Should return JSON with allowed, call_limit, etc.

Debug Mode

Add console.log statements to worker code for debugging:

console.log('[DEBUG] Phone normalized:', normalizedPhone); console.log('[DEBUG] Active tables:', activeTables); console.log('[DEBUG] Query result:', result);

Then view logs with wrangler tail

7. Accessing Workers in Cloudflare Dashboard

Overview

The Outbound Call Limiter has two separate worker deployments that can be managed through the Cloudflare dashboard:

  • Production: outbound-fitness-international
  • Development: outbound-fitness-international-dev
⚠️ Important: Always test changes in the dev environment before deploying to production!

Locating Your Workers

Step 1: Navigate to Workers & Pages

  1. Log in to Cloudflare Dashboard
  2. Select your account
  3. Click Workers & Pages in the left sidebar
  4. You'll see both workers listed:
    • outbound-fitness-international (Production)
    • outbound-fitness-international-dev (Development)
Worker List in Dashboard

Workers list showing dev and prod environments

Viewing and Editing Worker Code

Step 2: Access the Worker Editor

  1. Click on the worker name (e.g., outbound-fitness-international)
  2. Click the "Edit Code" button in the top right
  3. The code editor will open showing the current deployed worker.js file
Edit Code Button

Click "Edit Code" to view or modify the worker

🚨 Warning: Making changes directly in the dashboard editor will immediately deploy those changes! For production, always deploy via wrangler CLI after testing in dev.

What You Can Do in the Editor

  • ✅ View the current deployed code
  • ✅ Search through the code (Ctrl+F / Cmd+F)
  • ✅ Make quick hotfixes (emergency only)
  • ✅ Test changes with the "Save and Deploy" button
  • ✅ View console logs in real-time

Version History and Rollback

Step 3: Access Deployment History

  1. From the worker overview page, click the "Deployments" tab
  2. You'll see a list of all previous deployments with:
    • Version ID (e.g., cb7a4cb8-c86e-4722-985f-5227f1ace532)
    • Deployment date and time
    • Who deployed it
  3. Each deployment has a "..." menu on the right side
Version History and Rollback

Version history with rollback option

Rolling Back to a Previous Version

  1. Find the version you want to rollback to
  2. Click the "..." menu next to that version
  3. Select "Deploy version"
  4. Confirm the deployment
⚠️ Rollback Best Practices:
  • Always note the current version ID before rolling back
  • Check the deployment date to ensure you're selecting the right version
  • Test in dev environment first if possible
  • Monitor logs immediately after rollback

When to Use Dashboard vs. Wrangler CLI

Task Use Dashboard Use Wrangler CLI
View current code ✅ Quick and easy ❌ Requires local files
Emergency hotfix ✅ Fast for simple fixes ⚠️ Better for tested changes
Rollback deployment ✅ Visual interface, easy ⚠️ Requires version ID
Regular deployments ❌ No version control ✅ Recommended approach
View logs ✅ Real-time in editor ✅ More detailed with tail
View metrics ✅ Built-in analytics ❌ Not available

Common Dashboard Tasks

Viewing Real-Time Logs

  1. Open the worker in the code editor
  2. Click the "Logs" tab at the bottom
  3. Logs will stream in real-time as requests come in
  4. Use the filter box to search for specific messages

Checking Worker Metrics

  1. From the worker overview page, view the "Metrics" tab
  2. See graphs for:
    • Requests per second
    • Error rate
    • CPU time usage
    • Success rate
  3. Adjust time range (last hour, 24 hours, 7 days, etc.)

Managing Environment Variables

  1. From the worker overview, go to "Settings"
  2. Scroll to "Variables and Secrets"
  3. View or add environment variables:
    • TELNYX_CONNECTION_ID
    • UPLOAD_BEARER_TOKEN
  4. Add secrets (encrypted values like API keys):
    • TELNYX_API_KEY
💡 Pro Tip: Secret values are encrypted and can never be viewed after creation. If you need to change a secret, you must create a new one with the same name.

Quick Reference: Dashboard URLs

  • Production Worker: Dashboard Link
  • Dev Worker: Dashboard Link
  • D1 Database: Navigate to Storage & Databases → D1 → outbound_limiter_db
  • R2 Storage: Navigate to Storage & Databases → R2 → fitness-assets
  • Queues: Navigate to Queues → csv-split-queue / csv-processing-queue

8. Escalation Procedures

When to Escalate to Engineering

  • 🔴 Critical: All calls blocked system-wide for >15 minutes
  • 🔴 Critical: Database corruption or data loss
  • 🟡 High: CSV uploads failing repeatedly for >1 hour
  • 🟡 High: Worker errors affecting >10% of requests
  • 🟡 High: Telnyx integration broken (calls not bridging)
  • 🟢 Medium: Specific phone numbers not working correctly
  • 🟢 Medium: Performance degradation (slow responses)

Information to Collect Before Escalating

  1. Problem Description: Clear description of the issue and impact
  2. Time of Occurrence: When did the problem start?
  3. Affected Resources: Which phones/uploads/endpoints are affected?
  4. Error Messages: Copy full error messages from logs
  5. Steps to Reproduce: How to trigger the problem
  6. Logs: Export relevant logs:
    wrangler tail --config outbound-wrangler.toml --format pretty > logs.txt
  7. Database State: Export relevant tables:
    wrangler d1 export outbound_limiter_db --config outbound-wrangler.toml --output db-backup.sql

Emergency Contacts

Contact engineering team with collected information:

  • Email: engineering@example.com
  • Slack: #outbound-limiter-support
  • On-call: PagerDuty escalation policy

Temporary Workarounds

Bypass Call Limits (Emergency Only)

If system is blocking legitimate calls, temporarily remove phone from database:

curl -X DELETE "https://outbound-fitness-international.solutions-2bd.workers.dev/outbound/delete" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"phone": "5551234567"}'

Phone will then be allowed unlimited calls until next CSV upload

Force Table Switch

If upload stuck but data is in database, manually switch active table:

wrangler d1 execute outbound_limiter_db --config outbound-wrangler.toml \ --command "UPDATE active_table SET table_name='[\"outbound_numbers_1211_2\"]' WHERE id=1"

Clear Queue Backlog

If queue messages stuck, may need to clear and restart:

⚠️ WARNING: This will lose in-progress uploads. Only use if upload is truly stuck and cannot recover.
# Contact engineering team - queue clearing requires special permissions

Last Updated: December 2024
Worker Version: cb7a4cb8-c86e-4722-985f-5227f1ace532
Production URL: https://outbound-fitness-international.solutions-2bd.workers.dev