Skip to content

Cloudflare Workers

show.fm uses Cloudflare Workers for edge computing tasks that require low latency, async processing, or scheduled execution. Workers run independently of the main SvelteKit application and communicate via queues and shared database.

Architecture Overview

┌────────────────────────────────────────────────────────────────────────────┐
│                         WORKERS ARCHITECTURE                               │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                     Main App (Cloudflare Pages)                     │   │
│  │                      app.podcasterplus.com                          │   │
│  └──────────────────────────────┬──────────────────────────────────────┘   │
│                                 │                                          │
│            ┌────────────────────┼────────────────────┐                     │
│            │                    │                    │                     │
│            ▼                    ▼                    ▼                     │
│   ┌────────────────┐   ┌────────────────┐   ┌────────────────┐             │
│   │    Queues      │   │    Cron        │   │   HTTP         │             │
│   │  (async jobs)  │   │  (scheduled)   │   │  (on-demand)   │             │
│   └───────┬────────┘   └───────┬────────┘   └───────┬────────┘             │
│           │                    │                    │                      │
│   ┌───────▼────────────────────▼────────────────────▼─────────┐            │
│   │                                                           │            │
│   │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │            │
│   │  │  RSS Feed    │  │  Automation  │  │  Automation  │     │            │
│   │  │   Worker     │  │  Scheduler   │  │  Executor    │     │            │
│   │  │              │  │              │  │              │     │            │
│   │  │ feed.pod...  │  │  Cron: * * * │  │ Queue:       │     │            │
│   │  │              │  │              │  │ automation-  │     │            │
│   │  └──────────────┘  └──────────────┘  │ executions   │     │            │
│   │                                      └──────────────┘     │            │
│   │                                                           │            │
│   │  ┌──────────────┐  ┌──────────────┐                        │            │
│   │  │ Notification │  │ Notification │                        │            │
│   │  │  Scheduler   │  │  Executor    │                        │            │
│   │  │              │  │              │                        │            │
│   │  │  Cron: * * * │  │ Queue:       │                        │            │
│   │  │              │  │ notification-│                        │            │
│   │  └──────────────┘  │ deliveries   │                        │            │
│   │                    └──────────────┘                        │            │
│   │                                                           │            │
│   │  ┌──────────────┐  ┌──────────────┐                        │            │
│   │  │  Scheduled   │  │  Lifecycle   │                        │            │
│   │  │  Publisher   │  │  Manager     │                        │            │
│   │  │              │  │              │                        │            │
│   │  │  Cron: * * * │  │ Cron: daily  │                        │            │
│   │  └──────────────┘  └──────────────┘                        │            │
│   │                                                           │            │
│   │                    CLOUDFLARE WORKERS                     │            │
│   └───────────────────────────────────────────────────────────┘            │
│                                 │                                          │
│                                 ▼                                          │
│   ┌───────────────────────────────────────────────────────────────────┐    │
│   │                      Shared Infrastructure                        │    │
│   │                                                                   │    │
│   │   ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────────┐   │    │
│   │   │Hyperdrive│   │    KV    │   │  Queues  │   │   Supabase   │   │    │
│   │   │(pooling) │   │  Cache   │   │ Messages │   │  PostgreSQL  │   │    │
│   │   └──────────┘   └──────────┘   └──────────┘   └──────────────┘   │    │
│   │                                                                   │    │
│   └───────────────────────────────────────────────────────────────────┘    │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

Worker Documentation

WorkerPurposeTriggerDomain
RSS FeedGenerate podcast RSS feedsHTTPfeed.podcasterplus.com
Media DeliveryServe the media R2 bucket with parity; log audio downloads to WAE (media_requests)HTTPmedia.podcasterplus.com
Analytics RollupWAE datapoints → IAB v2.2-aligned daily download aggregates + analytics triggersCron (hourly at :05)Internal
Podcast Import ExecutorMirror source feed audio/assets into R2 (create + merge modes); resolve external episode linksQueue consumer (podcast-imports, external-episode-link)Internal
Lifecycle ManagerAuto-reactivate pauses, hard-delete podcasts, R2 cleanup, three fast-lane watchdogs (imports, external-link, stuck migration preview)Cron (daily 04:00 UTC + every minute)Internal
Automation SchedulerSchedule time-based automationsCron (every minute) + QueueInternal
Automation ExecutorExecute automation actionsQueue consumerInternal
Notification SchedulerClaim due notification deliveriesCron (every minute)Internal
Notification ExecutorDeliver notifications (email/push/in-app)Queue consumerInternal
Scheduled PublisherPublish episodes at scheduled timeCron (every minute)Internal
Public API & EmbedKeyless public JSON API v1 + embeddable player deliveryHTTP + Queue consumer (public-api-invalidation)api.podcasterplus.com, embed.podcasterplus.com
AI ProcessorRun AI jobs (transcription, content generation, guest research) as Cloudflare WorkflowsQueue consumer (ai-jobs) spawning Workflow instancesInternal
Media ProcessorMedia plane: audio normalisation (WAV→MP3 + FLAC master) and transcription-prep as Workflows driving ffmpeg ContainersQueue consumer (media-jobs) spawning Workflow instances + HTTP sweepInternal
Distribution MonitorCheck directory listing status; emit automation eventsCron (hourly)Internal
CRM SyncNightly CRM sync cycles + daily admin digestCron (03:35 + 06:00 UTC)Internal
DR ReplicatorReal-time R2 → Backblaze B2 media replicationQueue consumer (R2 event notifications)Internal

Two additional directories under workers/ are proof-of-concept code, not deployed production workers: ai-poc (transcript editor PoC; its editor logic was ported to src/lib/components/transcripts/editor.ts) and ai-research-poc (research calibration PoC used to size AI-credit costs). Production AI jobs run on the AI Processor.

Worker Categories

Public-Facing Workers

RSS Feed Worker generates standards-compliant podcast RSS feeds:

  • URL: https://rss.cdn.media/{podcast_id} (Epic 17: the UUID is the permanent key; slug-form paths answer a permanent 301 via the alias namespace)
  • Features: Multi-tier caching (Browser → Edge → KV), Podcast 2.0 support, ETag validation
  • Lifecycle awareness: Handles pending_deletion podcasts with <itunes:new-feed-url> tag (days 1-7) then 301 redirect (days 8+)
  • Performance: ~160ms cached response, ~250ms uncached
  • Triggers: HTTP requests, queue-based cache invalidation

Media Delivery Worker fronts the podcasterplus-media R2 bucket:

  • URL: https://media.podcasterplus.com/{object-key} (took over the hostname from the bare R2 custom domain)
  • Serving: full/ranged GETs, HEAD, conditional requests (200/206/304/412/416), ETag, caches.default edge caching
  • Ingest: episode-audio GETs logged to WAE dataset media_requests with a daily-rotating HMAC IP hash (no raw IPs)
  • Posture: delivery is sacred — logging is fire-and-forget and fail-open; no database access

Analytics Worker

Analytics Rollup turns raw request datapoints into durable download analytics:

  • Trigger: Cron hourly at :05 (5 * * * *)
  • Reads: WAE SQL API (media_requests + feed_requests), weighted by _sample_interval
  • Filter: IAB v2.2-aligned download rules (OPAWG bot lists, 60s byte threshold, fixed-UTC-day dedupe)
  • Writes: analytics_episode_daily / analytics_podcast_daily / analytics_episode_totals over Hyperdrive; stamps analytics_rollup_state
  • Triggers: milestone/spike crossing detection → automation-events queue + notify_analytics_milestone RPC

Lifecycle Management Worker

Lifecycle Manager handles automated podcast lifecycle transitions plus three fast-lane watchdogs:

  • Triggers: Cron daily at 04:00 UTC (pause/delete lane) and every minute (fast lane — three parallel sweeps)
  • Pause expiry: Auto-reactivates paused podcasts after 90-day maximum
  • Pending deletion: Hard-deletes podcasts after redirect window, cleans up R2 storage
  • Billing sync: Resumes Stripe billing after auto-reactivation (self-healing subscription ID resolution)
  • RSS invalidation: Invalidates cache after hard delete via queue or HTTP fallback
  • Stalled-import watchdog: Re-dispatches import_items rows past their per-attempt threshold under a running parent to recover from rare lost podcast-imports queue messages
  • Stalled external-link watchdog (Epic 11): Re-dispatches external-episode-link messages for episodes whose external_link_next_attempt_at is past due
  • Stuck hosting-migration preview watchdog (Epic 11): Cancels podcast_imports rows wedged in the preview staging window after a dead commit request

Automation System Workers

Automation Scheduler handles scheduling:

  • Time-Based: Cron runs every minute, finds due scheduled jobs
  • Event-Based: Consumes automation-events queue for instant triggers
  • Output: Creates execution records, enqueues to automation-executions

Automation Executor performs actions:

  • Actions: Send email (Resend), webhook calls, database field updates
  • Processing: Sequential action execution with magic tag replacement
  • Reliability: 5 retries, dead letter queue for failures

Notification System Workers

Notification Scheduler claims due deliveries:

  • Trigger: Cron every minute
  • Process: Query due/retryable deliveries → Claim with row locks → Enqueue to queue
  • Cleanup: Expired tracking tokens (30 days), old dead letters (90 days)
  • Throughput: Up to 200 deliveries per run (50 per page)

Notification Executor delivers notifications:

  • Channels: Email (Resend), Push (Web Push/VAPID), In-App (Supabase Realtime)
  • Pre-checks: Quiet hours, read state, hourly email cap, push subscription validity
  • Reliability: 4-tier retry (5/15/60/180 min), dead letter queue for permanent failures
  • Tracking: Email open/click/unsubscribe tokens, push click events

Publishing Worker

Scheduled Publisher auto-publishes episodes:

  • Trigger: Cron every minute
  • Process: Find due episodes → Update status → Invalidate RSS → Emit automation events
  • Safety: Only publishes episodes with audio_url set

Import Worker

Podcast Import Executor mirrors a source feed episode-by-episode:

  • Trigger: Queue consumer on podcast-imports (produced by the Imports API).
  • Process: Claim item → stream audio to R2 → mirror cover/chapters/transcripts → INSERT episodes → increment parent counter.
  • Concurrency: max_concurrency = 4 (polite to source hosts); max_batch_size = 1 (each message is a long-running stream).
  • Finalisation: Last outstanding item fires one rss-invalidation send + one Resend "import complete" email.

Common Infrastructure

Hyperdrive (Connection Pooling)

All Postgres-touching workers use Hyperdrive for their connections (the Media Delivery Worker is the exception — it never touches the database):

toml
# wrangler.toml
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "a81d477ff9264805989f5a72f0354ee8"
typescript
// Worker code
import postgres from 'postgres';

const sql = postgres(env.HYPERDRIVE.connectionString, {
  max: 1,           // Hyperdrive handles pooling
  prepare: false    // Required for Hyperdrive
});

Benefits: 10-100x faster cold starts vs direct connections.

Service bindings (same-zone worker-to-worker calls)

The main app runs as the Worker podcasterplus-app on the same zone as the other route-bound workers. A route-bound Worker's same-zone subrequests go direct to origin, bypassing Worker routes, and the feed./api. origins are the DR 100:: discard records, so a plain fetch to those hostnames times out (observed live, 2026-07-29). Internal HTTP calls therefore use service bindings:

Binding (on podcasterplus-app)Target workerUsed by
RSS_FEED_SERVICEpodcasterplus-rss-feed/api/rss/invalidate HTTP lane; audio-finalize feed invalidation
PUBLIC_API_SERVICEpodcasterplus-public-apipublic-api _internal/invalidate HTTP lane

The bindings are optional in code: absent (Vite local dev, which runs off-zone) the call sites fall back to plain fetch, and any failure still falls through to the queue lane. Any new server-side call from a route-bound worker to an own-zone worker hostname must follow the same pattern. Rule and background: .claude/rules/backend/workers.md.

Cloudflare Queues

Workers communicate via queues for async processing:

QueueProducerConsumerPurpose
rss-invalidationMain app, Publisher, Import ExecutorRSS Feed WorkerCache invalidation
podcast-importsMain app (Imports API + External Link API hosting-migration commit), Lifecycle Manager (watchdog)Podcast Import ExecutorPer-episode import / merge job
podcast-imports-dlqQueue retriesManual reviewFailed import items after 5 attempts
external-episode-linkMain app (publish-handoff, External Link API), Lifecycle Manager (watchdog), Podcast Import Executor (self-retry)Podcast Import ExecutorResolve a pending external episode to its RSS <guid>
external-episode-link-dlqQueue retriesManual reviewFailed link attempts after 2 Cloudflare retries
automation-eventsMain app, Analytics RollupAutomation SchedulerEvent triggers
automation-executionsSchedulerAutomation ExecutorAction execution
automation-dlqQueue retriesManual reviewFailed automation messages
notification-deliveriesNotification SchedulerNotification ExecutorNotification delivery
notification-deliveries-dlqQueue retriesManual reviewFailed notification deliveries

KV Namespace

RSS Feed Worker uses KV for global caching:

typescript
// Cache structure
feed:{slug}           // Full XML content (3600s TTL)
feed:{slug}:etag      // ETag hash for 304 responses
feed:{slug}:meta      // Generation metadata

Workers Analytics Engine

Raw request logging for the analytics pipeline (WAE holds ~90 days; the Postgres aggregates are the durable store):

DatasetWriterReaderContents
media_requestsMedia Delivery WorkerAnalytics Rollup (SQL API)Episode-audio download events
feed_requestsRSS Feed WorkerAnalytics Rollup (SQL API)Feed poll events

Blob order in each dataset is a positional contract with the rollup's SQL — see the Media Delivery and Analytics Rollup pages.

Deployment

Deploy Individual Worker

bash
cd workers/{worker-name}
npx wrangler deploy

Deploy All Workers

bash
# RSS Feed Worker
cd workers/rss-feed && npx wrangler deploy

# Lifecycle Manager
cd workers/lifecycle-manager && npx wrangler deploy

# Automation Workers
cd workers/automation-scheduler && npx wrangler deploy
cd workers/automation-executor && npx wrangler deploy

# Notification Workers
cd workers/notification-scheduler && npx wrangler deploy
cd workers/notification-executor && npx wrangler deploy

# Scheduled Publisher
cd workers/scheduled-publisher && npx wrangler deploy

# Podcast Import Executor
cd workers/podcast-import-executor && npx wrangler deploy

# Media Delivery
cd workers/media-delivery && npx wrangler deploy

# Analytics Rollup
cd workers/analytics-rollup && npx wrangler deploy

Set Secrets

bash
cd workers/{worker-name}

# Database access
npx wrangler secret put SUPABASE_SECRET_KEY

# Worker-specific secrets
npx wrangler secret put RESEND_API_KEY          # Executor only
npx wrangler secret put RESEND_FROM_EMAIL       # Executor only
npx wrangler secret put RSS_INVALIDATION_SECRET # RSS, Publisher
npx wrangler secret put IP_HASH_SECRET          # Media Delivery, RSS (shared value)
npx wrangler secret put CF_ANALYTICS_API_TOKEN  # Analytics Rollup (Account Analytics Read)

View Logs

bash
# Real-time logs
npx wrangler tail {worker-name}

# Example
npx wrangler tail podcasterplus-rss-feed
npx wrangler tail podcasterplus-automation-scheduler

Error Handling

Queue Retry Configuration

All queue consumers are configured for automatic retries:

toml
[[queues.consumers]]
queue = "automation-executions"
max_batch_size = 25
max_batch_timeout = 30
max_retries = 5                    # Automatic retries
dead_letter_queue = "automation-dlq"  # Final destination for failures

Failure Isolation

Workers isolate failures to prevent cascading issues:

typescript
for (const item of batch) {
  try {
    await processItem(item);
  } catch (error) {
    // Log error but continue processing other items
    console.error(JSON.stringify({ event: 'item_failed', error: String(error) }));
  }
}

Monitoring

Key Metrics

MetricTargetAlert Threshold
RSS Feed latency< 200ms> 500ms
Queue depth< 100> 1000
DLQ size0> 0
Cron success rate100%< 99%
Action failure rate< 1%> 5%

Logging Format

All workers use structured JSON logging:

typescript
console.log(JSON.stringify({
  event: 'action_completed',
  worker: 'automation-executor',
  execution_id: '...',
  duration_ms: 150
}));

Worker Communication Flow

Development

Local Development

bash
cd workers/{worker-name}
npx wrangler dev

Testing Queues Locally

bash
# Send test message to queue
npx wrangler queues send automation-events '{"trigger_type":"booking.confirmed",...}'

Create Queues

bash
npx wrangler queues create rss-invalidation
npx wrangler queues create automation-events
npx wrangler queues create automation-executions
npx wrangler queues create automation-dlq
npx wrangler queues create notification-deliveries
npx wrangler queues create notification-deliveries-dlq

Internal documentation - Not for public distribution