โ Validate Domains
๐ Overviewโ
The Validate Domains job synchronizes custom domain status from Cloudflare to the DashClicks Lightning Domains database. It runs every 5 minutes, fetching all custom hostnames from Cloudflare, comparing their status with local database records, and updating changed domains. When a domain becomes inactive or has SSL issues, it automatically reviews and updates the status of all funnels using that domain.
Complete Flow:
- Cron Initialization:
queue-manager/crons/domains/validate.js - Service Processing:
queue-manager/services/domains/validate.js - Queue Definition:
queue-manager/queues/domains/update_validity.js
Execution Pattern: Scheduled polling (every 5 minutes)
Queue Name: domains_update_validity
Environment Flag: QM_DOMAINS_VALIDATE=true (in index.js)
๐ Complete Processing Flowโ
sequenceDiagram
participant CRON as Cron Schedule<br/>(every 5 min)
participant SERVICE as Validate Service
participant CF_API as Cloudflare API
participant QUEUE as Bull Queue
participant PROCESSOR as Job Processor
participant LD_DB as Lightning<br/>Domains DB
participant FUNNEL_DB as Funnels DB
CRON->>SERVICE: validate()
SERVICE->>CF_API: GET /custom_hostnames<br/>page 1
CF_API-->>SERVICE: {result[], result_info}
SERVICE->>SERVICE: Calculate total pages:<br/>result_info.total_pages
SERVICE->>QUEUE: Add page 1 job:<br/>{page_num:1, page_data}
loop For pages 2..totalPages
SERVICE->>QUEUE: Add job:<br/>{page_num: N}
end
loop Each queued page
QUEUE->>PROCESSOR: Process page job
alt Page data not provided
PROCESSOR->>CF_API: GET /custom_hostnames?page=N
CF_API-->>PROCESSOR: {result[]}
end
PROCESSOR->>PROCESSOR: Extract CF domain IDs
PROCESSOR->>LD_DB: Find domains:<br/>cf_id IN domain_ids
LD_DB-->>PROCESSOR: foundDomains[]
loop Each found domain
PROCESSOR->>PROCESSOR: Compare:<br/>CF status vs DB status<br/>CF SSL status vs DB SSL
alt Status changed
PROCESSOR->>LD_DB: updateOne:<br/>Update domain record
alt Domain not active
PROCESSOR->>FUNNEL_DB: reviewAndSetStatusForDomain:<br/>Set funnels to test mode
end
end
end
PROCESSOR-->>QUEUE: done()
end
๐ Source Filesโ
1. Cron Initializationโ
File: queue-manager/crons/domains/validate.js
Purpose: Schedule domain validation every 5 minutes
Cron Pattern: */5 * * * * (every 5 minutes)
Initialization:
const validate = require('../../services/domains/validate');
const cron = require('node-cron');
const logger = require('../../utilities/logger');
let inProgress = false;
exports.start = async () => {
try {
cron.schedule('*/5 * * * *', async () => {
if (!inProgress) {
inProgress = true;
await validate();
inProgress = false;
}
});
} catch (err) {
logger.error({ initiator: 'QM/domains/validate', error: err });
}
};
In-Progress Lock: Prevents overlapping validations if Cloudflare API is slow.
2. Service Processing (THE CLOUDFLARE PAGINATION LOGIC)โ
File: queue-manager/services/domains/validate.js
Purpose: Fetch Cloudflare custom hostnames and queue paginated processing
Key Functions:
- Fetch first page from Cloudflare API
- Calculate total pages from
result_info - Queue page 1 with embedded data (optimization)
- Queue remaining pages without data (lazy load)
Main Processing Function:
const { default: axios } = require('axios');
const Queue = require('../../queues/domains/update_validity');
const logger = require('../../utilities/logger');
module.exports = async () => {
try {
// Handle the promise returned by axios.get
await axios
.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
)
.then(async page1 => {
const totalPages = page1.data.result_info.total_pages || 1;
const queue = await Queue.start();
// Queue page 1 with embedded data (avoids redundant API call)
queue.add(
{ page_num: 1, page_data: page1.data.result },
{
attempts: 3,
},
);
// Queue remaining pages without data (lazy load in processor)
for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
queue.add(
{ page_num: pageNum },
{
attempts: 3,
},
);
}
logger.log({
initiator: 'QM/domains/validate',
message: 'Domain Validation service processed.',
});
});
} catch (err) {
logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
}
};
Pagination Optimization:
- Page 1: Data embedded in job (avoids redundant API call)
- Pages 2+: Only page number provided (processor fetches lazily)
- Reduces memory usage for large datasets
3. Queue Processor (THE SYNC & UPDATE LOGIC)โ
File: queue-manager/queues/domains/update_validity.js
Purpose: Process each page of Cloudflare domains and sync to database
Key Functions:
- Lazy load page data if not provided
- Fetch matching domains from database
- Compare Cloudflare status with database status
- Update changed domains
- Review funnel status for inactive domains
Main Processor:
const mongoose = require('mongoose');
const { socketEmit } = require('../../utilities');
const { verifyBalance } = require('../../utilities/onebalance');
const QueueWrapper = require('../../common/queue-wrapper');
const LightningDomain = require('../../models/lightning-domain');
const logger = require('../../utilities/logger');
const { reviewAndSetStatusForDomain } = require('../../utilities/funnelDomainStatusUtility');
const { default: axios } = require('axios');
const processCb = async (job, done) => {
try {
let { page_num, page_data } = job.data;
// Lazy load page data if not provided
if (!page_data) {
const response = await axios.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames?page=${page_num}`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
);
page_data = response.data?.result;
}
// Get related records from db
const { foundDomains, notFoundDomains } = await fetchLightningDomains(page_data);
if (foundDomains.length) {
await updateLightningDomains(foundDomains, page_data);
}
// Commented out: Delete unwanted domains
// if (notFoundDomains.length) {
// await deleteUnwantedDomains(notFoundDomains);
// }
done();
} catch (err) {
logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
done(err);
}
};
let queue;
exports.start = async () => {
try {
if (!queue) queue = QueueWrapper(`domains_update_validity`, 'global', { processCb });
return Promise.resolve(queue)
.then(result => {
logger.log({
initiator: 'QM/domains/update_validity',
message: 'Queue initialized:' + result,
});
return result;
})
.catch(err => {
logger.error({
initiator: 'QM/domains/update_validity',
error: 'Error initializing queue' + err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
throw err;
});
} catch (err) {
logger.error({
initiator: 'QM/domains/update_validity',
error: 'Error while initializing domain validity queue' + err.message + err.stack,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
throw err; // Re-throw to ensure the error is properly propagated
}
};
const fetchLightningDomains = async domains => {
const domain_ids = domains.map(d => d.id);
const foundDomains = await LightningDomain.find({ cf_id: { $in: domain_ids } });
const notFoundDomains = domains.reduce((acc, curr) => {
if (!foundDomains.find(d => d?.cf_id == curr.id)) acc.push(curr);
return acc;
}, []);
return { foundDomains, notFoundDomains };
};
const updateLightningDomains = async (foundDomains, pageData) => {
const updatePromises = [];
for (let fd of foundDomains) {
const domain = pageData.find(({ id }) => id == fd.cf_id);
if (domain && (domain.status != fd.status || domain.ssl.status != fd.ssl.status)) {
// Update domain record
updatePromises.push(
LightningDomain.updateOne({ _id: fd._id }, { ...domain })
.then(result => {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Updated domain ${fd._id}:${result}`,
});
return result;
})
.catch(err => {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Error updating domain ${fd._id}:${err}`,
});
throw err;
}),
);
// If domain not active, review and update funnel status
if (domain.status !== 'active') {
await reviewAndSetStatusForDomain({ domain_id: fd._id })
.then(result => {
logger.log({
initiator: 'QM/domains/update_validity',
message: `Status reviewed for domain ${fd._id}: ` + result,
});
return result;
})
.catch(err => {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Error reviewing status for domain ${fd._id}:${err}`,
});
throw err;
});
}
}
}
await Promise.allSettled(updatePromises).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
logger.log({
initiator: 'QM/domains/update_validity',
message: `Update fulfilled:': ` + result.value,
});
} else {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Update rejected:${result?.reason}`,
});
}
});
});
};
๐๏ธ Collections Usedโ
lightning_domainsโ
- Operations: Find (query by cf_id), Update (sync status from Cloudflare)
- Model:
shared/models/lightning-domain.js - Usage Context: Store and sync custom domain status
Query Criteria (Fetch Matching Domains):
{
cf_id: {
$in: domain_ids;
} // Cloudflare custom hostname IDs
}
Update Operation:
await LightningDomain.updateOne(
{ _id: fd._id },
{ ...domain }, // Spread Cloudflare domain object
);
Key Fields:
cf_id: Cloudflare custom hostname ID (e.g.,'a1b2c3d4-5678-90ab-cdef-1234567890ab')status: Domain status ('active','pending','pending_validation','blocked')ssl.status: SSL certificate status ('active','pending','pending_validation')
Cloudflare Domain Object Structure:
{
id: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', // cf_id
hostname: 'custom.example.com',
status: 'active', // Domain status
ssl: {
status: 'active', // SSL status
method: 'http',
type: 'dv',
certificate_authority: 'lets_encrypt',
validation_errors: []
},
ownership_verification: {
type: 'txt',
name: '_cf-custom-hostname.custom.example.com',
value: 'abc123...'
},
created_at: '2024-01-15T10:30:00Z'
}
funnels (Indirect via Utility)โ
- Operations: Update (via
reviewAndSetStatusForDomainutility) - Model:
shared/models/funnel.js - Usage Context: Set funnels to test mode when domain becomes inactive
Utility Function: utilities/funnelDomainStatusUtility.js
Purpose: Updates all funnels using an inactive domain to prevent broken live funnels
๐ง Job Configurationโ
Cron Scheduleโ
'*/5 * * * *'; // Every 5 minutes
Why 5-Minute Interval?
- Balances timely status updates with API rate limits
- Cloudflare status changes are typically not immediate
- Avoids excessive API calls
Job Settingsโ
queue.add(
{ page_num: 1, page_data: page1.data.result }, // Page 1 with data
{ attempts: 3 }, // Retry up to 3 times
);
queue.add(
{ page_num: 2 }, // Page 2+ without data
{ attempts: 3 }, // Retry up to 3 times
);
Job Data:
page_num: Page number (1-based)page_data: Optional pre-fetched Cloudflare domain array (only page 1)
Queue Configurationโ
QueueWrapper(`domains_update_validity`, 'global', { processCb });
Queue Name: domains_update_validity
Redis Scope: global (shared across queue manager instances)
๐ Processing Logic - Detailed Flowโ
1. Cloudflare API Paginationโ
First Request (Service Layer):
await axios.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
);
Response Structure:
{
result: [
{ id: 'cf123...', hostname: 'domain1.com', status: 'active', ssl: {...} },
{ id: 'cf456...', hostname: 'domain2.com', status: 'pending', ssl: {...} },
// ... up to 50 domains per page (Cloudflare default)
],
result_info: {
page: 1,
per_page: 50,
count: 50,
total_count: 150,
total_pages: 3 // Calculate from this
},
success: true,
errors: [],
messages: []
}
Calculate Total Pages:
const totalPages = page1.data.result_info.total_pages || 1;
2. Job Queuing Strategyโ
Optimization for Page 1:
queue.add(
{ page_num: 1, page_data: page1.data.result }, // Embed data
{ attempts: 3 },
);
Why Embed Page 1 Data?
- Already fetched in service layer
- Avoids redundant API call in processor
- Reduces Cloudflare API usage
Lazy Load for Pages 2+:
for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
queue.add(
{ page_num: pageNum }, // No data, fetch in processor
{ attempts: 3 },
);
}
Why Not Embed All Pages?
- Memory efficiency (each page ~50 domains ร 1KB = 50KB)
- Parallel processing (Bull queue handles concurrency)
- API rate limit distribution
3. Lazy Data Loading (Processor Layer)โ
Conditional Fetch:
let { page_num, page_data } = job.data;
if (!page_data) {
const response = await axios.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames?page=${page_num}`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
);
page_data = response.data?.result;
}
Request URL Example:
GET https://api.cloudflare.com/client/v4/zones/abc123.../custom_hostnames?page=2
4. Domain Matchingโ
Extract Cloudflare IDs:
const domain_ids = domains.map(d => d.id);
// ['cf123...', 'cf456...', 'cf789...']
Query Database:
const foundDomains = await LightningDomain.find({ cf_id: { $in: domain_ids } });
Identify Orphaned Domains (Commented Out):
const notFoundDomains = domains.reduce((acc, curr) => {
if (!foundDomains.find(d => d?.cf_id == curr.id)) acc.push(curr);
return acc;
}, []);
Why Track Not Found Domains?
- Cloudflare domains without database records
- Possible orphaned domains from deleted funnels
- Cleanup logic currently disabled
5. Status Comparison & Updateโ
Detect Changes:
const domain = pageData.find(({ id }) => id == fd.cf_id);
if (domain && (domain.status != fd.status || domain.ssl.status != fd.ssl.status)) {
// Status changed - update required
}
Comparison Logic:
| Cloudflare Status | DB Status | Update Needed? | Action |
|---|---|---|---|
active | active | โ No | Skip |
active | pending | โ Yes | Update + Review Funnels |
pending | active | โ Yes | Update + Set Funnels to Test |
pending_validation | pending | โ Yes | Update |
SSL Status Comparison (Separate Check):
domain.ssl.status != fd.ssl.status;
SSL Statuses:
active: Certificate valid and installedpending: Certificate issuance in progresspending_validation: Awaiting domain validationfailed: Certificate issuance failed
6. Database Updateโ
Spread Operator Pattern:
await LightningDomain.updateOne({ _id: fd._id }, { ...domain });
What Gets Updated (Cloudflare Object Spread):
{
id: 'cf123...', // cf_id
hostname: 'custom.example.com',
status: 'active', // Main status
ssl: { // SSL object
status: 'active',
method: 'http',
type: 'dv',
certificate_authority: 'lets_encrypt',
validation_errors: []
},
ownership_verification: {...},
created_at: '2024-01-15T10:30:00Z',
// ... other Cloudflare fields
}
Warning: This spreads the entire Cloudflare object, which may include fields not in the Mongoose schema. Mongoose will ignore undefined schema fields, but this pattern can lead to data inconsistencies.
7. Funnel Status Reviewโ
Triggered When Domain Becomes Inactive:
if (domain.status !== 'active') {
await reviewAndSetStatusForDomain({ domain_id: fd._id });
}
Utility Function (utilities/funnelDomainStatusUtility.js):
Purpose: Set all funnels using this domain to test mode
Logic (inferred):
// Find all funnels using this domain
const funnels = await Funnel.find({ domain_id: fd._id, status: 'live' });
// Set to test mode to prevent broken live funnels
await Funnel.updateMany({ domain_id: fd._id, status: 'live' }, { status: 'test' });
Why Set to Test Mode?
- Prevents users from accessing broken funnels
- Domain may have SSL errors or be blocked
- Test mode allows fixing without affecting live traffic
8. Promise Settlement Patternโ
Using Promise.allSettled (Not Promise.all):
await Promise.allSettled(updatePromises).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
logger.log({ message: `Update fulfilled: ${result.value}` });
} else {
logger.error({ message: `Update rejected: ${result.reason}` });
}
});
});
Why allSettled Instead of all?
Promise.all: Fails fast on first rejection (stops other updates)Promise.allSettled: Waits for all promises, logs each success/failure- Better for batch operations where partial success is acceptable
๐จ Error Handlingโ
Common Error Scenariosโ
Cloudflare API Rate Limitโ
Scenario: Too many requests to Cloudflare API
Error:
{
errors: [{ code: 10000, message: 'rate limit exceeded' }];
}
Result: Job fails, retries up to 3 times with backoff.
Recovery: Exponential backoff allows rate limit to reset.
Invalid Cloudflare Zone IDโ
Error:
{
errors: [{ code: 1001, message: 'Invalid zone_id' }];
}
Result: Service-level error, logged but no retries (configuration issue).
Database Connection Errorโ
Scenario: MongoDB connection lost during domain update
Result: Job fails, retries up to 3 times.
Recovery: Mongoose reconnection logic handles transient failures.
Domain Update Conflictโ
Scenario: Concurrent updates to same domain (race condition)
Result: One update succeeds, others may fail with version conflict.
Recovery: Retry handles eventual consistency.
Commented-Out Cleanup Logicโ
Orphaned Domain Deletion (Disabled):
// if (notFoundDomains.length) {
// await deleteUnwantedDomains(notFoundDomains);
// }
Why Disabled?
- Risky operation (permanent deletion)
- May delete domains still in use
- Requires careful validation logic before re-enabling
๐ Monitoring & Loggingโ
Success Loggingโ
Service-Level:
logger.log({
initiator: 'QM/domains/validate',
message: 'Domain Validation service processed.',
});
Update Success:
logger.log({
initiator: 'QM/domains/update_validity',
message: `Updated domain ${fd._id}:${result}`,
});
Funnel Review Success:
logger.log({
initiator: 'QM/domains/update_validity',
message: `Status reviewed for domain ${fd._id}: ${result}`,
});
Error Loggingโ
Service-Level Errors:
logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
Processor Errors:
logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
Individual Update Errors:
logger.error({
initiator: 'QM/domains/update_validity',
message: `Error updating domain ${fd._id}:${err}`,
});
Performance Metricsโ
- Service Query Time: 1-3 seconds (Cloudflare API latency)
- Job Processing Time: 100-500ms per page (50 domains)
- Total Job Time: 5-15 seconds for 150 domains (3 pages)
- Typical Updates: 0-10 domains per run (status changes are infrequent)
๐ Integration Pointsโ
Triggers This Jobโ
- Cron Schedule: Every 5 minutes automatically
- Manual Trigger: Via API endpoint (if QM_HOOKS=true)
External Dependenciesโ
- Cloudflare API: Custom Hostnames API
- Zone ID:
process.env.LD_CLOUDFLARE_ZONE_ID - API Key:
process.env.CLOUDFLARE_API_KEY - Rate Limit: 1,200 requests per 5 minutes (default)
- Zone ID:
Jobs That Depend On Thisโ
- Domain Renewal: Checks domain status before renewal
- Domain Cancellation: Validates status before cancellation
- Funnel Publishing: Verifies domain is active before publishing
Utilities Usedโ
reviewAndSetStatusForDomain: Updates funnel status when domain becomes inactivesocketEmit: (Imported but unused) Potential real-time status updatesverifyBalance: (Imported but unused) Potential balance checks for domain renewals
โ ๏ธ Important Notesโ
Side Effectsโ
- โ ๏ธ Domain Record Update: Overwrites entire domain record with Cloudflare data
- โ ๏ธ Funnel Status Change: Sets funnels to test mode when domain becomes inactive
- โ ๏ธ SSL Status Tracking: Updates SSL certificate status independently
Performance Considerationsโ
- Pagination: Processes 50 domains per page (Cloudflare default)
- Concurrent Processing: Bull queue handles multiple pages in parallel
- API Rate Limits: Monitor Cloudflare rate limits (1,200 req/5min)
- Database Load: Batch
$inqueries reduce database round trips
Business Logicโ
Why Validate Every 5 Minutes?
- Domain status changes are infrequent but time-sensitive
- SSL validation can take 1-10 minutes
- 5-minute interval balances timeliness with API costs
Why Update Funnels on Inactive Domains?
- Prevents users from accessing broken funnels
- Maintains user trust (no broken experiences)
- Allows admin intervention before re-enabling
Why Embed Page 1 Data?
- Optimization: Avoids redundant API call
- Reduces API usage by ~33% (for 3-page dataset)
- Negligible memory overhead (50KB)
Maintenance Notesโ
- Cloudflare API Keys: Rotate periodically, stored in environment variables
- Zone ID: Tied to specific Cloudflare account, rarely changes
- Orphaned Domain Cleanup: Currently disabled, requires careful validation before re-enabling
- Schema Spread Pattern: Consider explicit field mapping instead of spreading entire Cloudflare object
Potential Issuesโ
Spread Operator Risk:
await LightningDomain.updateOne({ _id: fd._id }, { ...domain });
Problem: Spreads entire Cloudflare object, may include fields not in schema.
Recommendation: Use explicit field mapping:
await LightningDomain.updateOne(
{ _id: fd._id },
{
status: domain.status,
ssl: domain.ssl,
ownership_verification: domain.ownership_verification,
// ... only fields defined in schema
},
);
๐งช Testingโ
Manual Triggerโ
# Via API (if QM_HOOKS=true)
POST http://localhost:6002/api/trigger/domains/validate
Simulate Cloudflare Status Changeโ
Using Cloudflare Dashboard:
- Navigate to SSL/TLS โ Custom Hostnames
- Select a domain
- Force re-validation or change status
- Wait up to 5 minutes for sync
Verify Update:
const domain = await LightningDomain.findOne({ hostname: 'custom.example.com' });
console.log('Status:', domain.status);
console.log('SSL Status:', domain.ssl.status);
Test Paginationโ
// Create test domains in Cloudflare (use API or dashboard)
// Ensure > 50 domains to trigger pagination
// Run validation
await validate();
// Check logs for page processing
// Expected: "Domain Validation service processed."
// Expected: Multiple job logs for each page
Test Funnel Status Reviewโ
// Setup: Create funnel with custom domain
const funnel = await Funnel.create({
name: 'Test Funnel',
domain_id: domain._id,
status: 'live',
});
// Simulate domain becoming inactive in Cloudflare
// (Use Cloudflare API to change domain status)
// Run validation
await validate();
// Verify funnel set to test mode
const updatedFunnel = await Funnel.findById(funnel._id);
console.log('Funnel status:', updatedFunnel.status); // Should be 'test'
Monitor Cloudflare API Usageโ
// Track API calls
let apiCallCount = 0;
// Intercept axios requests
const originalGet = axios.get;
axios.get = async (...args) => {
if (args[0].includes('cloudflare')) {
apiCallCount++;
console.log(`Cloudflare API call #${apiCallCount}: ${args[0]}`);
}
return originalGet(...args);
};
// Run validation
await validate();
// Expected calls: 1 (service) + totalPages (processor) = 1 + 3 = 4 calls
console.log('Total Cloudflare API calls:', apiCallCount);
Verify Status Change Detectionโ
// Setup: Domain with status change
const domain = await LightningDomain.findOne({});
const originalStatus = domain.status;
// Manually change status in Cloudflare
// (Use Cloudflare API)
// Run validation
await validate();
// Verify update
const updatedDomain = await LightningDomain.findById(domain._id);
console.log('Original status:', originalStatus);
console.log('Updated status:', updatedDomain.status);
console.log('Status changed:', originalStatus !== updatedDomain.status);
Job Type: Scheduled + Queued
Execution Frequency: Every 5 minutes
Average Duration: 5-15 seconds (150 domains across 3 pages)
Status: Active