Skip to content

Comments

Updated the scaling factors for domain warmup#3

Open
tomerqodo wants to merge 2 commits intocoderabbit_full_base_updated_the_scaling_factors_for_domain_warmup_pr3from
coderabbit_full_head_updated_the_scaling_factors_for_domain_warmup_pr3
Open

Updated the scaling factors for domain warmup#3
tomerqodo wants to merge 2 commits intocoderabbit_full_base_updated_the_scaling_factors_for_domain_warmup_pr3from
coderabbit_full_head_updated_the_scaling_factors_for_domain_warmup_pr3

Conversation

@tomerqodo
Copy link

@tomerqodo tomerqodo commented Jan 29, 2026

Benchmark PR from agentic-review-benchmarks#3

Summary by CodeRabbit

  • New Features

    • Enhanced domain warming scaling with high-volume support and refined threshold handling for gradual email ramping.
  • Bug Fixes

    • Improved email scaling calculations for better control across varying send volumes.

Word count: 28 words

✏️ Tip: You can customize this high-level summary in your review settings.

sam-lord and others added 2 commits January 25, 2026 11:56
closes
[GVA-605](https://linear.app/ghost/issue/GVA-605/adjust-the-warmup-numbers-to-reflect-joeys-suggestions)

Our initial scaling factors were based on some preliminary research, and
were designed to be as simple as possible. After further discussions,
we've decided to avoid having such a fast ramp-up at the early stage,
and limit the growth at the later stages to 75k+ per send. That should
still prevent any large sites from going back into warm-up between
sends, but is more likely to avoid damaging the reputation by scaling
too aggressively.
@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

The domain warming service is refactored to use paged email queries with findPage instead of findOne, replaces the defaultScale field with a highVolume configuration, and updates scaling logic to apply conservative multipliers (1.25×–2×) based on new thresholds instead of simple doubling.

Changes

Cohort / File(s) Summary
Core Implementation
ghost/core/core/server/services/email-service/DomainWarmingService.ts
Updated #getHighestCount to use findPage (returning array) instead of findOne. Enhanced #getTargetLimit with new highVolume threshold cap, refined scaling thresholds (added 1k and 5k levels; adjusted 100k and 400k values), and adjusted loop condition from <= to <. Replaced defaultScale with highVolume configuration object containing threshold, maxScale, and maxAbsoluteIncrease fields.
Integration Tests
ghost/core/test/integration/services/email-service/domain-warming.test.js
Replaced per-call timing with fixed baseDate for deterministic test execution. Updated expected limit calculations to use 1.25× scaling instead of doubling. Added getExpectedScale helper returning scale factors: 200 for ≤100, 1.25× for 100–1k, 1.5× for 1k–5k, 1.75× for 5k+. Updated day-based progression assertions (Day 1: 200, Day 2: 250, Day 3: 313).
Unit Tests
ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts
Replaced findOne stubs with findPage stubs returning paged results ({data: [...]} format). Updated test setup and assertions to verify paging behavior with filter, order, and limit parameters. Refactored warmup progression expectations to align with new 1.25×–2× scaling multipliers and highVolume threshold logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A warming dance with new-found grace,
Paged queries quicken up the pace,
From doubling bold to scaling wise,
Thresholds guide with 1.25,
Little hops instead of leaps so bold! 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: adjusting domain warmup scaling factors. It directly relates to the primary objective and is reflected in the code changes across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@ghost/core/core/server/services/email-service/DomainWarmingService.ts`:
- Around line 131-138: The threshold comparison in the warm-up scaling loop is
exclusive which misclassifies exact boundary counts; update the check inside the
for-loop iterating over WARMUP_SCALING_TABLE.thresholds (and any similar logic)
to use an inclusive comparison (<=) when comparing lastCount with
threshold.limit so exact boundary values fall into the intended tier, and ensure
the final fallback still uses WARMUP_SCALING_TABLE.highVolume.maxScale
unchanged.
- Around line 101-112: Update the date filter in
DomainWarmingService.#getHighestCount to exclude today by using '<' instead of
'<=' when calling this.#emailModel.findPage and replace the loose null check on
the retrieved value from result.data[0].get('csd_email_count') with a strict
nullish check (e.g., use !== null/!== undefined or the nullish coalescing
operator) so you return the actual numeric count or 0 without violating eqeqeq.

In
`@ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts`:
- Around line 186-210: The expected value for the 400000 case is wrong given the
service's highVolume.threshold = 400_000; update the test in the "should return
correct warmup progression through the stages" unit (the testCases array) so the
entry {lastCount: 400000, expected: 800000} becomes {lastCount: 400000,
expected: 475000} (min(1.2×, lastCount + 75k) => min(480000, 475000) = 475000);
keep the rest of the cases unchanged.

Comment on lines +101 to +112
const result = await this.#emailModel.findPage({
filter: `created_at:<=${new Date().toISOString().split('T')[0]}`,
order: 'csd_email_count DESC',
limit: 1
});

if (!email) {
if (!result.data.length) {
return 0;
}

const count = email.get('csd_email_count');
return count || 0;
const count = result.data[0].get('csd_email_count');
return count != null ? count : 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exclude today correctly and satisfy eqeqeq in #getHighestCount.

The filter currently uses <=, which doesn’t match the “excluding today” intent and fails the unit test expecting <. Also, count != null trips eqeqeq. Consider strict null checks.

🛠️ Proposed fix
-        const result = await this.#emailModel.findPage({
-            filter: `created_at:<=${new Date().toISOString().split('T')[0]}`,
+        const result = await this.#emailModel.findPage({
+            filter: `created_at:<${new Date().toISOString().split('T')[0]}`,
             order: 'csd_email_count DESC',
             limit: 1
         });
@@
-        const count = result.data[0].get('csd_email_count');
-        return count != null ? count : 0;
+        const count = result.data[0].get('csd_email_count');
+        return (count === null || count === undefined) ? 0 : count;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await this.#emailModel.findPage({
filter: `created_at:<=${new Date().toISOString().split('T')[0]}`,
order: 'csd_email_count DESC',
limit: 1
});
if (!email) {
if (!result.data.length) {
return 0;
}
const count = email.get('csd_email_count');
return count || 0;
const count = result.data[0].get('csd_email_count');
return count != null ? count : 0;
const result = await this.#emailModel.findPage({
filter: `created_at:<${new Date().toISOString().split('T')[0]}`,
order: 'csd_email_count DESC',
limit: 1
});
if (!result.data.length) {
return 0;
}
const count = result.data[0].get('csd_email_count');
return (count === null || count === undefined) ? 0 : count;
🧰 Tools
🪛 ESLint

[error] 112-112: Expected '!==' and instead saw '!='.

(eqeqeq)

🤖 Prompt for AI Agents
In `@ghost/core/core/server/services/email-service/DomainWarmingService.ts` around
lines 101 - 112, Update the date filter in DomainWarmingService.#getHighestCount
to exclude today by using '<' instead of '<=' when calling
this.#emailModel.findPage and replace the loose null check on the retrieved
value from result.data[0].get('csd_email_count') with a strict nullish check
(e.g., use !== null/!== undefined or the nullish coalescing operator) so you
return the actual numeric count or 0 without violating eqeqeq.

Comment on lines 131 to +138
for (const threshold of WARMUP_SCALING_TABLE.thresholds.sort((a, b) => a.limit - b.limit)) {
if (lastCount <= threshold.limit) {
if (lastCount < threshold.limit) {
return Math.ceil(lastCount * threshold.scale);
}
}

return Math.ceil(lastCount * WARMUP_SCALING_TABLE.defaultScale);
// This should not be reached given the thresholds cover all cases up to highVolume.threshold
return Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make threshold checks inclusive to match docs/tests.

Using < shifts exact boundary values (e.g., 1,000) into the next tier, which contradicts the doc table and unit test expectations.

🛠️ Proposed fix
-            if (lastCount < threshold.limit) {
+            if (lastCount <= threshold.limit) {
                 return Math.ceil(lastCount * threshold.scale);
             }
🤖 Prompt for AI Agents
In `@ghost/core/core/server/services/email-service/DomainWarmingService.ts` around
lines 131 - 138, The threshold comparison in the warm-up scaling loop is
exclusive which misclassifies exact boundary counts; update the check inside the
for-loop iterating over WARMUP_SCALING_TABLE.thresholds (and any similar logic)
to use an inclusive comparison (<=) when comparing lastCount with
threshold.limit so exact boundary values fall into the intended tier, and ensure
the final fallback still uses WARMUP_SCALING_TABLE.highVolume.maxScale
unchanged.

Comment on lines 186 to 210
it('should return correct warmup progression through the stages', async function () {
// Test the complete warmup progression
// New conservative scaling:
// - Base: 200 for counts ≤100
// - 1.25× until 1k (conservative early ramp)
// - 1.5× until 5k (moderate increase)
// - 1.75× until 100k (faster ramp after proving deliverability)
// - 2× until 400k
// - High volume (400k+): min(1.2×, lastCount + 75k) to avoid huge jumps
const testCases = [
{lastCount: 0, expected: 200},
{lastCount: 50, expected: 200},
{lastCount: 100, expected: 200},
{lastCount: 200, expected: 400},
{lastCount: 500, expected: 1000},
{lastCount: 1000, expected: 2000},
{lastCount: 50000, expected: 100000},
{lastCount: 100000, expected: 200000},
{lastCount: 200000, expected: 300000},
{lastCount: 400000, expected: 600000},
{lastCount: 500000, expected: 625000},
{lastCount: 800000, expected: 1000000}
{lastCount: 200, expected: 250}, // 200 × 1.25 = 250
{lastCount: 500, expected: 625}, // 500 × 1.25 = 625
{lastCount: 1000, expected: 1250}, // 1000 × 1.25 = 1250
{lastCount: 2000, expected: 3000}, // 2000 × 1.5 = 3000
{lastCount: 5000, expected: 7500}, // 5000 × 1.5 = 7500
{lastCount: 50000, expected: 87500}, // 50000 × 1.75 = 87500
{lastCount: 100000, expected: 175000}, // 100000 × 1.75 = 175000
{lastCount: 200000, expected: 400000}, // 200000 × 2 = 400000
{lastCount: 400000, expected: 800000}, // 400000 × 2 = 800000
{lastCount: 500000, expected: 575000}, // min(500000 × 1.2, 500000 + 75000) = min(600000, 575000)
{lastCount: 800000, expected: 875000} // min(800000 × 1.2, 800000 + 75000) = min(960000, 875000)
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

400k boundary expectation conflicts with the high‑volume cap.

With highVolume.threshold = 400_000, a lastCount of 400k should be capped to min(1.2×, +75k) (475k), not doubled. If you intended 2× at 400k, the service threshold/condition should change instead.

🛠️ Proposed fix (align with current service config)
-            // - 2× until 400k
-            // - High volume (400k+): min(1.2×, lastCount + 75k) to avoid huge jumps
+            // - 2× until <400k
+            // - High volume (>=400k): min(1.2×, lastCount + 75k) to avoid huge jumps
@@
-                {lastCount: 400000, expected: 800000}, // 400000 × 2 = 800000
+                {lastCount: 400000, expected: 475000}, // min(400000 × 1.2, 400000 + 75000) = 475000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should return correct warmup progression through the stages', async function () {
// Test the complete warmup progression
// New conservative scaling:
// - Base: 200 for counts ≤100
// - 1.25× until 1k (conservative early ramp)
// - 1.5× until 5k (moderate increase)
// - 1.75× until 100k (faster ramp after proving deliverability)
// - 2× until 400k
// - High volume (400k+): min(1.2×, lastCount + 75k) to avoid huge jumps
const testCases = [
{lastCount: 0, expected: 200},
{lastCount: 50, expected: 200},
{lastCount: 100, expected: 200},
{lastCount: 200, expected: 400},
{lastCount: 500, expected: 1000},
{lastCount: 1000, expected: 2000},
{lastCount: 50000, expected: 100000},
{lastCount: 100000, expected: 200000},
{lastCount: 200000, expected: 300000},
{lastCount: 400000, expected: 600000},
{lastCount: 500000, expected: 625000},
{lastCount: 800000, expected: 1000000}
{lastCount: 200, expected: 250}, // 200 × 1.25 = 250
{lastCount: 500, expected: 625}, // 500 × 1.25 = 625
{lastCount: 1000, expected: 1250}, // 1000 × 1.25 = 1250
{lastCount: 2000, expected: 3000}, // 2000 × 1.5 = 3000
{lastCount: 5000, expected: 7500}, // 5000 × 1.5 = 7500
{lastCount: 50000, expected: 87500}, // 50000 × 1.75 = 87500
{lastCount: 100000, expected: 175000}, // 100000 × 1.75 = 175000
{lastCount: 200000, expected: 400000}, // 200000 × 2 = 400000
{lastCount: 400000, expected: 800000}, // 400000 × 2 = 800000
{lastCount: 500000, expected: 575000}, // min(500000 × 1.2, 500000 + 75000) = min(600000, 575000)
{lastCount: 800000, expected: 875000} // min(800000 × 1.2, 800000 + 75000) = min(960000, 875000)
];
it('should return correct warmup progression through the stages', async function () {
// Test the complete warmup progression
// New conservative scaling:
// - Base: 200 for counts ≤100
// - 1.25× until 1k (conservative early ramp)
// - 1.5× until 5k (moderate increase)
// - 1.75× until 100k (faster ramp after proving deliverability)
// - 2× until <400k
// - High volume (>=400k): min(1.2×, lastCount + 75k) to avoid huge jumps
const testCases = [
{lastCount: 0, expected: 200},
{lastCount: 50, expected: 200},
{lastCount: 100, expected: 200},
{lastCount: 200, expected: 250}, // 200 × 1.25 = 250
{lastCount: 500, expected: 625}, // 500 × 1.25 = 625
{lastCount: 1000, expected: 1250}, // 1000 × 1.25 = 1250
{lastCount: 2000, expected: 3000}, // 2000 × 1.5 = 3000
{lastCount: 5000, expected: 7500}, // 5000 × 1.5 = 7500
{lastCount: 50000, expected: 87500}, // 50000 × 1.75 = 87500
{lastCount: 100000, expected: 175000}, // 100000 × 1.75 = 175000
{lastCount: 200000, expected: 400000}, // 200000 × 2 = 400000
{lastCount: 400000, expected: 475000}, // min(400000 × 1.2, 400000 + 75000) = 475000
{lastCount: 500000, expected: 575000}, // min(500000 × 1.2, 500000 + 75000) = min(600000, 575000)
{lastCount: 800000, expected: 875000} // min(800000 × 1.2, 800000 + 75000) = min(960000, 875000)
];
🤖 Prompt for AI Agents
In
`@ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts`
around lines 186 - 210, The expected value for the 400000 case is wrong given
the service's highVolume.threshold = 400_000; update the test in the "should
return correct warmup progression through the stages" unit (the testCases array)
so the entry {lastCount: 400000, expected: 800000} becomes {lastCount: 400000,
expected: 475000} (min(1.2×, lastCount + 75k) => min(480000, 475000) = 475000);
keep the rest of the cases unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants