Skip to content

Background jobs

Zimmer runs on GoodJob. In development it’s :async (in-process with Puma); in production and staging it’s :external, requiring a separate bundle exec good_job start.

From config.good_job.cron:

CadenceJobWhat it does
30sHeartbeatSweepJobNudge needs_input sessions with a heartbeat enabled
30sGitHubPullRequestPollerJobPoll CI status on sessions with a PR URL
30sGithubCommentPollerJobPoll PR review comments
1mSlackTriggerPollerJobPoll Slack channels for trigger conditions
1mScheduleTriggerJobFire due schedule triggers
2mGitHubMergeConflictPollerJobDetect merge conflicts on open PRs
2mCliStatusRefreshJobRefresh the gh / claude / codex version cache
5mCleanupOrphanedSessionsJobSessions marked running whose process is gone
5mRefreshRuntimeAuthTokensJobRefresh Anthropic/OpenAI OAuth tokens
5mCleanupExpiredElicitationsJobExpire elicitations + clear stranded blocks
5mCleanupRuntimeLoginAttemptsJobReap abandoned login attempts
10mTranscriptArchiveJobRebuild latest.zip
15mCatalogRefreshJobair update + reload the catalog
15mQuotaResetCheckerJobRestore quota_exceeded Claude accounts
15mRefreshXOauthTokensJobRefresh X/Twitter tokens
30mRefreshMcpOauthTokensJobRefresh MCP OAuth tokens expiring within the hour
hourlyStaleCloneCleanupJobReap clones from archived sessions
hourly :45SlackTriggerHealthCheckJobDetect Slack feeds that silently stopped firing
ZombieReaperJob, DeferredCloneCleanupJob, EmptyTrashJob, DockerCleanupJob, OrphanCloneFilesystemCleanupJob, SystemHealthMonitorJob, CertExpiryMonitorJob, EgressHealthCheckJobcleanup and monitoring

Most jobs run on default. Two are deliberately isolated:

  • :triggersAoEventTriggerJob and ScheduleTriggerJob. They were previously starved on default; AoEventTriggerJob::DISPATCH_LATENCY_WARN_THRESHOLD = 120s exists because of it.
  • :pollers with total_limit: 1SlackTriggerPollerJob. SlackService retries up to 10 times with a blocking 1-second sleep inside the job thread, and the comment admits this would “saturate the queue’s whole thread pool.”
ServiceWhat it handles
SigtermRetryServiceDeploys and OOM kills. MAX_RETRIES = 3
ApiErrorRetryServiceVendor API errors; classifies quota vs transient
ContextLengthRetryServiceContext overflow. MAX_RETRIES = 2 — “after 2 attempts we assume compaction isn’t helping”
AuthRecoveryServiceMid-run auth loss
SessionRecoveryServiceHung processes. Explicitly “best-effort”
NpxCacheHealServiceA corrupted _npx cache — detected by regexing npm’s stderr
GlobalRateLimitTrackerSIGTERM/529 pressure counter driving adaptive backoff

BroadcastService wraps Turbo broadcasts in a hand-rolled circuit breaker: THRESHOLD = 5 failures, RESET_TIME = 60 seconds, MAX_RETRIES = 3.

When it trips, live UI updates stop for 60 seconds. The session keeps running; you can’t see it. There’s no banner telling you the breaker is open.

AlertService has a DEDUP_WINDOW = 1.hour — a genuinely new instance of the same alert inside an hour is swallowed. AlertBatcher truncates aggregated bodies at MAX_AGGREGATED_DETAILS_CHARS = 2700.