Overview
Datadog monitors for ATS sync health and outbound ATS request-queue health. Slack channels:
Related tickets: TEN-350 (sync alerting), TEN-502 (stuck CREATING_APPLICATION), TEN-576 (reason-tagged abandon / hang alerts).
Slack format: alerts are one-line headers with links here. Recoveries do not post to Slack (Datadog UI only). Investigate in the alert thread.
Compact Slack notifications
To keep#dd-sync-alerts and #dd-ats-request-queue readable, alerts use three layers:
Datadog still adds a short attachment title, metric value (metric monitors), and built-in action buttons (Mute, Declare incident, Create case, Investigate with Bits). Mute is enabled via
mute_buttons: true; the other buttons are native Datadog Slack app features.
Re-apply monitor messages + compact Slack settings (requires SALV_DD_API_KEY / SALV_DD_APP_KEY with monitor-write and manage-integrations):
#dd-sync-alerts
Kombo ATS sync failed (webhook)
Monitor: 309657484 · Query:KomboSyncNotSucceeded log count > 0 in 30m, per ats_integration_id
What to check
- Kombo integration health / sync logs for this
ats_integration_id sync_stateon matching logs:failed,partially_failed,authentication_failed, etc.- Logs
authentication_failed webhooks are often recurring Kombo auth drift — treat separately from one-off sync failures.
ATS sync queue enqueue failed
Canonical metric:ats.sync.queue.errors (publisher-side; every recorded publish failure, including permanent skips)
Monitor: 309657487 · Query: ats.sync.stream.errors{error_type:publish*} > 0 in 30m, per integration_id + sync_type
The monitor still matches the dual-write of ats.sync.stream.errors (error_type:publish*). Prefer ats.sync.queue.errors in new dashboards; the monitor query can switch later without a code change.
Tags
class:retry— send failed, cycle timeout, or unexpected publish exception. Cursor is not advanced; the next cycle retries.class:permanent— message cannot fit Service Bus (reason:message_too_large). Cursor is advanced so the queue cannot wedge on an unsizable payload.reason:send_failed|message_too_large|publish_timeout|publish_exception- Also tagged with
sync_type,integration_id,org_id,ats_provider,topic
- Service Bus health / publish errors for this integration
- Sync controller logs around job/applicant/candidate/tearsheet/placement queue publish
- Permanent skips: logs with
error.kind:AtsSyncPublishPermanentSkip - Metric
- Companion dual-write: stream.errors publish*
Cursor advance rule
Every sync type decides whether to move its cursor with one rule, evaluated top to bottom, first match wins. The rule lives inserver/ats/sync_watermark.py (decide_watermark_advance).
The cursor always moves to the sync start time, never the end time, so records modified during the run fall inside the next window.
A publish timeout or exception never reaches the rule: the phase exits before deciding, the cursor is simply not written, and the timeout logs a warning while a non-transient exception logs an error with a traceback. A job publish exception also cancels the applicant and tearsheet phases running alongside it.
When the job publish completes but leaves its cursor held (retryable item failures), the applicant, tearsheet and placement phases still run to completion, and the cycle is then reported as failed for that integration. The tearsheet and placement stream publishers send one message at a time and stop at the first failure, so they never produce permanent skips.
ATS sync queue lag > 1 hour
Monitor: 309657489 · Query:ats.sync.queue.lag_seconds > 3600 in 15m, per integration_id + sync_type
What to check
- Service Bus subscription backlog / worker health for this sync type
- Whether this integration is publishing faster than workers can process
- Metric
- Companion:
ats.sync.queue.subscription.active_messages
active_messages before deep-diving.
ATS sync hang abandon
Monitor: 314970801 · Query:ats.sync.queue.abandoned{class:hang} > 0 in 15m, per integration_id + sync_type + reason
Fires when a sync worker (or the session worker) abandons a message for a hang-class reason. Retry-class abandons (ats_fetch_failed, transient sync errors, client-init failures, …) emit the same metric but do not Slack — they are normal Service Bus redelivery.
lock_lost is included at > 0: production had 0 "Lost message/session lock during process_batch" logs in the 7d before this monitor shipped. Large multi-chunk batches are allowed to overrun 900s; if lock_lost becomes noisy, raise that reason’s threshold rather than dropping the rest of the hang class.
This is the signal for applicant/candidate 600s bulk-fetch timeouts (reason:ats_fetch_timeout). The duration monitor below does not catch those — they return ABANDON around 600s, under the 720s duration threshold.
Which monitor fires
Reason → log grep
What to check
- Metric explorer:
ats.sync.queue.abandoned{env:production,class:hang}grouped byreason,sync_type,integration_id - Companion:
ats.sync.queue.handler.duration_secondsand queue lag for the same integration - Job/tearsheet/placement fetch errors without a 600s wrapper surface as
lock_lostand/or duration, notats_fetch_timeout
ATS sync handler near session lock
Monitor: 314970802 · Query:ats.sync.queue.handler.duration_seconds > 720 in 15m, per integration_id + sync_type
80% of lock_renewal_seconds=900. Catches batches still running toward lock expiry (and slow completes). Does not fire for the 600s applicant/candidate fetch-timeout path — use hang abandon reason:ats_fetch_timeout for that.
Duration is now emitted on session-worker exception paths (lock_lost, unhandled process_batch) as well as successful handler returns.
What to check
- Whether this integration’s receive batch / chunk size is too large for the 900s session lock
- Hang abandon for the same
integration_id(lock_lostvs still-in-progress) - Metric
Candidates stuck in transient Tenzo stage
Monitor: 309683123 · Query:ats.candidate.stuck_transient_stage.count > 0 in 15m, per org_id + stage
What we detect
Stage entry age is the
created_at of the latest matching tenzo_stage_history row where new_stage equals the CCI’s current tenzo_stage (ordered by history_seq desc). If no history row exists, we fall back to candidate_campaign_info.updated_at.
Soft-deleted CCIs and campaigns are excluded. Only non-deleted rows are counted.
The policy list in code is extensible (DEFAULT_TRANSIENT_STAGE_STUCK_POLICIES in server/ats/stuck_transient_stage_metrics.py); only CREATING_APPLICATION ships initially. Do not add AWAITING_RESUME_REVIEW here until that alert is intentionally scoped.
Metrics
Emitted every 5 minutes from the queue server (skipped whenLOCAL or test).
Tags:
org_id, stage. (integration_id is omitted — it lives in Cosmos, not on the CCI row used for this query.)
What to check
- List stuck rows via
candidate_campaign_ats_sync_dao.list_stuck_transient_stage_candidates(see Ops inspection below) - Applicant sync lag:
ats.sync.queue.lag_seconds{sync_type:applicant,env:production} - Logs:
Deferred CCI link for native-fallback,remains in CREATING_APPLICATION,Applicant sync worker timed out bulk-fetching applications - Metric
#dd-ats-request-queue
ATS request queue failure rate > 5%
Monitor: 309673813 · Query:(http_error + timeout + exception) / completed > 5% in 30m, only if ≥100 completed requests, per integration_id
What to check
- ATS/provider health and 4xx/5xx responses for this integration
- Queue depth / capacity:
ats.request_queue.depth,ats.request_queue.rate_limiter.capacity_remaining - Request-queue service logs for this
integration_id
ATS request queue HTTP errors spike
Monitor: 309673821 · Query:ats.request_queue.http_error > 10 in 30m, per integration_id
Absolute spike alert — catches high-volume integrations with many failures even if rate stays under 5%.
What to check
- Request-queue logs for HTTP error responses (Kombo passthrough vs direct provider)
- Provider status pages / incident history
- Whether failures cluster on one endpoint (e.g. stage writebacks)
ATS request queue timeouts
Monitor: 309673814 · Query:ats.request_queue.timeout > 0 in 15m, per integration_id
What to check
ats.request_queue.depthandin_flightfor this integration- Attachment / long-running requests monopolizing workers
- Request-queue service logs
ATS request queue exceptions
Monitor: 309673815 · Query:ats.request_queue.exception > 0 in 15m, per integration_id
What to check
- Request-queue exception logs for this integration
- Provider connectivity / DNS / TLS issues
- Recent deploys of the ATS request-queue service
ATS request queue depth high
Monitor: 309673819 · Query:ats.request_queue.depth warn >15 / critical >50 in 15m, per integration_id
What to check
- Worker pool / rate-limit capacity for this integration
- Attachment storms / sync storms
ats.request_queue.in_flightandcapacity_remaining
ATS request queue rate-limit capacity exhausted
Monitor: 309673820 · Query: avgats.request_queue.rate_limiter.capacity_remaining < 1 in 15m, per integration_id
What to check
- Whether 429s are appearing in request-queue logs
- Sync storm vs normal traffic for this integration
- Provider RPM budget in rate-limiter config
Ops inspection (stuck transient stage)
DAO helper (preferred)
Read-only, bounded list (defaultlimit=100), oldest first:
Equivalent SQL sketch
Implementation pointers
This path does not use incident.io or
operational_alerts/.