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
| Worker | Purpose | Trigger | Domain |
|---|---|---|---|
| RSS Feed | Generate podcast RSS feeds | HTTP | feed.podcasterplus.com |
| Media Delivery | Serve the media R2 bucket with parity; log audio downloads to WAE (media_requests) | HTTP | media.podcasterplus.com |
| Analytics Rollup | WAE datapoints → IAB v2.2-aligned daily download aggregates + analytics triggers | Cron (hourly at :05) | Internal |
| Podcast Import Executor | Mirror source feed audio/assets into R2 (create + merge modes); resolve external episode links | Queue consumer (podcast-imports, external-episode-link) | Internal |
| Lifecycle Manager | Auto-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 Scheduler | Schedule time-based automations | Cron (every minute) + Queue | Internal |
| Automation Executor | Execute automation actions | Queue consumer | Internal |
| Notification Scheduler | Claim due notification deliveries | Cron (every minute) | Internal |
| Notification Executor | Deliver notifications (email/push/in-app) | Queue consumer | Internal |
| Scheduled Publisher | Publish episodes at scheduled time | Cron (every minute) | Internal |
| Public API & Embed | Keyless public JSON API v1 + embeddable player delivery | HTTP + Queue consumer (public-api-invalidation) | api.podcasterplus.com, embed.podcasterplus.com |
| AI Processor | Run AI jobs (transcription, content generation, guest research) as Cloudflare Workflows | Queue consumer (ai-jobs) spawning Workflow instances | Internal |
| Media Processor | Media plane: audio normalisation (WAV→MP3 + FLAC master) and transcription-prep as Workflows driving ffmpeg Containers | Queue consumer (media-jobs) spawning Workflow instances + HTTP sweep | Internal |
| Distribution Monitor | Check directory listing status; emit automation events | Cron (hourly) | Internal |
| CRM Sync | Nightly CRM sync cycles + daily admin digest | Cron (03:35 + 06:00 UTC) | Internal |
| DR Replicator | Real-time R2 → Backblaze B2 media replication | Queue 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_deletionpodcasts 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.defaultedge caching - Ingest: episode-audio GETs logged to WAE dataset
media_requestswith 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_totalsover Hyperdrive; stampsanalytics_rollup_state - Triggers: milestone/spike crossing detection →
automation-eventsqueue +notify_analytics_milestoneRPC
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_itemsrows past their per-attempt threshold under arunningparent to recover from rare lostpodcast-importsqueue messages - Stalled external-link watchdog (Epic 11): Re-dispatches
external-episode-linkmessages for episodes whoseexternal_link_next_attempt_atis past due - Stuck hosting-migration preview watchdog (Epic 11): Cancels
podcast_importsrows wedged in thepreviewstaging 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-eventsqueue 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_urlset
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-invalidationsend + 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):
# wrangler.toml
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "a81d477ff9264805989f5a72f0354ee8"// 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 worker | Used by |
|---|---|---|
RSS_FEED_SERVICE | podcasterplus-rss-feed | /api/rss/invalidate HTTP lane; audio-finalize feed invalidation |
PUBLIC_API_SERVICE | podcasterplus-public-api | public-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:
| Queue | Producer | Consumer | Purpose |
|---|---|---|---|
rss-invalidation | Main app, Publisher, Import Executor | RSS Feed Worker | Cache invalidation |
podcast-imports | Main app (Imports API + External Link API hosting-migration commit), Lifecycle Manager (watchdog) | Podcast Import Executor | Per-episode import / merge job |
podcast-imports-dlq | Queue retries | Manual review | Failed import items after 5 attempts |
external-episode-link | Main app (publish-handoff, External Link API), Lifecycle Manager (watchdog), Podcast Import Executor (self-retry) | Podcast Import Executor | Resolve a pending external episode to its RSS <guid> |
external-episode-link-dlq | Queue retries | Manual review | Failed link attempts after 2 Cloudflare retries |
automation-events | Main app, Analytics Rollup | Automation Scheduler | Event triggers |
automation-executions | Scheduler | Automation Executor | Action execution |
automation-dlq | Queue retries | Manual review | Failed automation messages |
notification-deliveries | Notification Scheduler | Notification Executor | Notification delivery |
notification-deliveries-dlq | Queue retries | Manual review | Failed notification deliveries |
KV Namespace
RSS Feed Worker uses KV for global caching:
// Cache structure
feed:{slug} // Full XML content (3600s TTL)
feed:{slug}:etag // ETag hash for 304 responses
feed:{slug}:meta // Generation metadataWorkers Analytics Engine
Raw request logging for the analytics pipeline (WAE holds ~90 days; the Postgres aggregates are the durable store):
| Dataset | Writer | Reader | Contents |
|---|---|---|---|
media_requests | Media Delivery Worker | Analytics Rollup (SQL API) | Episode-audio download events |
feed_requests | RSS Feed Worker | Analytics 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
cd workers/{worker-name}
npx wrangler deployDeploy All Workers
# 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 deploySet Secrets
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
# Real-time logs
npx wrangler tail {worker-name}
# Example
npx wrangler tail podcasterplus-rss-feed
npx wrangler tail podcasterplus-automation-schedulerError Handling
Queue Retry Configuration
All queue consumers are configured for automatic retries:
[[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 failuresFailure Isolation
Workers isolate failures to prevent cascading issues:
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
| Metric | Target | Alert Threshold |
|---|---|---|
| RSS Feed latency | < 200ms | > 500ms |
| Queue depth | < 100 | > 1000 |
| DLQ size | 0 | > 0 |
| Cron success rate | 100% | < 99% |
| Action failure rate | < 1% | > 5% |
Logging Format
All workers use structured JSON logging:
console.log(JSON.stringify({
event: 'action_completed',
worker: 'automation-executor',
execution_id: '...',
duration_ms: 150
}));Worker Communication Flow
Development
Local Development
cd workers/{worker-name}
npx wrangler devTesting Queues Locally
# Send test message to queue
npx wrangler queues send automation-events '{"trigger_type":"booking.confirmed",...}'Create Queues
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-dlqRelated Documentation
- Cloudflare Services - Infrastructure configuration
- Notification System - Centralised notification delivery system
- Automation Engine - Business logic for automation system
- Supabase Integration - Database patterns used by workers
- Testing Patterns - Worker testing with mocks