fix: reduce docket redis polling from 250ms to 5s (#559)

the default docket worker polls redis every 250ms (4x/sec), which
caused ~3.4M redis commands in 2 days and $6.73 in upstash costs.

changes:
- add check_interval_seconds and scheduling_resolution_seconds to DocketSettings
- default both to 5 seconds (20x reduction in polling frequency)
- update background.py Worker to use these settings
- update audd costs in export_costs.py ($3.91 -> $9.13 for current period)

expected redis cost reduction: ~95% for idle polling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub c9fe8e43 57b60c65

Changed files
+20 -4
backend
src
backend
scripts
+8
backend/src/backend/_internal/background.py
··· 15 import logging 16 from collections.abc import AsyncGenerator 17 from contextlib import asynccontextmanager 18 19 from docket import Docket, Worker 20 ··· 69 async with Worker( 70 docket, 71 concurrency=settings.docket.worker_concurrency, 72 ) as worker: 73 worker_task = asyncio.create_task( 74 worker.run_forever(),
··· 15 import logging 16 from collections.abc import AsyncGenerator 17 from contextlib import asynccontextmanager 18 + from datetime import timedelta 19 20 from docket import Docket, Worker 21 ··· 70 async with Worker( 71 docket, 72 concurrency=settings.docket.worker_concurrency, 73 + # reduce polling frequency to save Redis costs (default is 250ms) 74 + minimum_check_interval=timedelta( 75 + seconds=settings.docket.check_interval_seconds 76 + ), 77 + scheduling_resolution=timedelta( 78 + seconds=settings.docket.scheduling_resolution_seconds 79 + ), 80 ) as worker: 81 worker_task = asyncio.create_task( 82 worker.run_forever(),
+8
backend/src/backend/config.py
··· 576 default=10, 577 description="Number of concurrent tasks per worker", 578 ) 579 580 581 class RateLimitSettings(AppSettingsSection):
··· 576 default=10, 577 description="Number of concurrent tasks per worker", 578 ) 579 + check_interval_seconds: float = Field( 580 + default=5.0, 581 + description="How often to check for new tasks (seconds). Default 5s reduces Redis costs vs docket's 250ms default.", 582 + ) 583 + scheduling_resolution_seconds: float = Field( 584 + default=5.0, 585 + description="How often to run the scheduler loop (seconds). Default 5s reduces Redis costs vs docket's 250ms default.", 586 + ) 587 588 589 class RateLimitSettings(AppSettingsSection):
+4 -4
scripts/costs/export_costs.py
··· 60 # the copyright_scans table was created Nov 24 but first scan recorded Nov 30 61 # so we hardcode this period from AudD dashboard. DELETE THIS after Dec 24 - 62 # future periods will use live database counts. 63 - # source: https://dashboard.audd.io - checked 2025-12-09 64 "audd": { 65 - "total_requests": 6781, 66 "included_requests": 6000, # 1000 + 5000 bonus 67 - "billable_requests": 781, 68 "cost_per_request": 0.005, # $5 per 1000 69 - "cost": 3.91, # 781 * $0.005 70 "note": "copyright detection API (indie plan)", 71 }, 72 }
··· 60 # the copyright_scans table was created Nov 24 but first scan recorded Nov 30 61 # so we hardcode this period from AudD dashboard. DELETE THIS after Dec 24 - 62 # future periods will use live database counts. 63 + # source: https://dashboard.audd.io - checked 2025-12-10 64 "audd": { 65 + "total_requests": 7826, # 6000 included + 1826 billable 66 "included_requests": 6000, # 1000 + 5000 bonus 67 + "billable_requests": 1826, 68 "cost_per_request": 0.005, # $5 per 1000 69 + "cost": 9.13, # 1826 * $0.005 70 "note": "copyright detection API (indie plan)", 71 }, 72 }