Skip to content

Commit 02273e4

Browse files
authored
No capped price for cloud workers (windmill-labs#843)
1 parent bd81503 commit 02273e4

File tree

5 files changed

+202
-94
lines changed

5 files changed

+202
-94
lines changed

docs/core_concepts/9_worker_groups/index.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,10 @@ Only tags for jobs that have been delayed by more than 3 seconds in the last 14
410410
411411
## Workers and compute units
412412
413-
Even though Windmill's architecture relies on workers, Windmill's [pricing](/pricing) is based on compute units. A compute unit corresponds to 2 worker-gb-month. For example, a worker with 2GB of memory limit (standard worker) counts as 1 compute unit. A worker with 4GB of memory (large worker) counts as 2 compute units. Any worker with memory above 2GB counts as 2 compute units (16GB worker counts as 2 compute units). Each worker can run up to ~26M jobs per month (at 100ms per job).
413+
Even though Windmill's architecture relies on workers, Windmill's [pricing](/pricing) is based on compute units. A compute unit corresponds to 2 worker-gb-month. For example, a worker with 2GB of memory limit (standard worker) counts as 1 compute unit. A worker with 4GB of memory counts as 2 compute units. On self-hosted plans, any worker with memory above 2GB counts as 2 compute units (16GB worker counts as 2 compute units). Each worker can run up to ~26M jobs per month (at 100ms per job).
414414
415415
The number of compute units will depend on the workload and the jobs Windmill will need to run. Each worker only executes one job at a time, by design to use the full resource of the worker. Workers come in different sizes based on memory: small (1GB), standard (2GB), and large (> 2GB). Each worker is extremely efficient to execute a job, and you can execute up to 26 million jobs per month per worker if each one lasts 100ms. However, it completely depends on the nature of the jobs, their number and duration.
416416
417-
As a note, keep in mind that the number of compute units considered is the number of production compute units of your workers, not of development staging, if you have separate instances. You can set staging instances as 'Non-prod' in the [Instance settings](../../advanced/18_instance_settings/index.mdx#non-prod-instance). The compute units are calculated based on the memory limits set in [docker-compose](https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml) or in Kubernetes. For example, a standard worker with 2GB memory counts as 1 compute unit, while a large worker with >2GB memory counts as 2 compute units. Any worker with memory above 2GB still counts as 2 compute units. Small workers are counted as 0.5 compute unit.
417+
As a note, keep in mind that the number of compute units considered is the number of production compute units of your workers, not of development staging, if you have separate instances. You can set staging instances as 'Non-prod' in the [Instance settings](../../advanced/18_instance_settings/index.mdx#non-prod-instance). The compute units are calculated based on the memory limits set in [docker-compose](https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml) or in Kubernetes. For example, a standard worker with 2GB memory counts as 1 compute unit, while a large worker with >2GB memory counts as 2 compute units (on self-hosted plans, any worker with memory above 2GB still counts as 2 compute units Small workers are counted as 0.5 compute unit.
418418
419-
Also, for the [Enterprise Edition](/pricing), the free trial of one month is meant to help you evaluate your needs in practice.
419+
Also, for the [Enterprise Edition](/pricing), the free trial of one month is meant to help you evaluate your needs in practice.

src/components/Pricing.js

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ function QualificationModal({ isOpen, closeModal, planType, qualificationText, s
366366
onChange={(e) => setQualificationText(e.target.value)}
367367
className="w-full p-3 text-base border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
368368
rows={6}
369-
placeholder={planType === 'SMB'
370-
? 'Please explain how you qualify for the SMB plan (e.g., small business size, revenue, etc.).'
369+
placeholder={planType === 'Pro'
370+
? 'Please explain how you qualify for the Pro plan (e.g., small business size, revenue, etc.).'
371371
: 'Please explain how you qualify for the Nonprofit plan (e.g., organization status, registration number, etc.).'}
372372
/>
373373
</div>
@@ -397,7 +397,7 @@ export default function Pricing() {
397397
const [frequency, setFrequency] = useState(types[1]);
398398
const [period, setPeriod] = useState(periods[0]);
399399

400-
const buttonOptions = ['SMB', 'Nonprofit', 'Enterprise'];
400+
const buttonOptions = ['Pro', 'Nonprofit', 'Enterprise'];
401401

402402
const [selectedOption, setSelectedOption] = useState('Enterprise');
403403
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -425,7 +425,7 @@ export default function Pricing() {
425425
};
426426

427427
const handlePlanClick = (url, planType) => {
428-
if (planType === 'SMB' || planType === 'Nonprofit') {
428+
if (planType === 'Pro' || planType === 'Nonprofit') {
429429
setIsModalOpen(true);
430430
setPendingUrl(url);
431431
} else {
@@ -544,7 +544,7 @@ export default function Pricing() {
544544
className={classNames(
545545
tier.id === 'tier-team'
546546
? 'ring-1 ring-blue-600'
547-
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
547+
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
548548
? 'ring-1 ring-blue-600'
549549
: tier.enterprise_edition
550550
? 'ring-1 ring-teal-600'
@@ -558,7 +558,7 @@ export default function Pricing() {
558558
className={classNames(
559559
tier.id === 'tier-team'
560560
? 'text-blue-600'
561-
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
561+
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
562562
? 'text-blue-600'
563563
: tier.enterprise_edition
564564
? 'text-teal-600'
@@ -569,8 +569,8 @@ export default function Pricing() {
569569
{
570570
tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Nonprofit'
571571
? tier.name_nonprofit // Display name for Nonprofit
572-
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
573-
? tier.name_smb // Display name for SMB
572+
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
573+
? tier.name_smb // Display name for Pro
574574
: tier.name // Default name
575575
}
576576
</h3>
@@ -601,13 +601,13 @@ export default function Pricing() {
601601
{period.value === 'annually'
602602
? tier.id === 'tier-team'
603603
? (tier.minPrice * 12).toLocaleString('en-US') // Team tier calculation
604-
: selectedOption === 'SMB' && tier.minPrice_smb !== undefined
605-
? (tier.minPrice_smb * 10).toLocaleString('en-US') // Annual price for SMB
604+
: selectedOption === 'Pro' && tier.minPrice_smb !== undefined
605+
? (tier.minPrice_smb * 10).toLocaleString('en-US') // Annual price for Pro
606606
: selectedOption === 'Nonprofit' && tier.minPrice_nonprofit !== undefined
607607
? (tier.minPrice_nonprofit * 10).toLocaleString('en-US') // Annual price for Nonprofit
608608
: (tier.minPrice * 10).toLocaleString('en-US') // Annual price for others
609-
: selectedOption === 'SMB' && tier.minPrice_smb !== undefined
610-
? tier.minPrice_smb.toLocaleString('en-US') // Monthly price for SMB
609+
: selectedOption === 'Pro' && tier.minPrice_smb !== undefined
610+
? tier.minPrice_smb.toLocaleString('en-US') // Monthly price for Pro
611611
: selectedOption === 'Nonprofit' && tier.minPrice_nonprofit !== undefined
612612
? tier.minPrice_nonprofit.toLocaleString('en-US') // Monthly price for Nonprofit
613613
: tier.minPrice.toLocaleString('en-US')}
@@ -633,7 +633,7 @@ export default function Pricing() {
633633
? 'bg-gray-500 text-white'
634634
: 'bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-gray-300',
635635
'ring-1 ring-inset ring-white dark:ring-zinc-800 hover:bg-gray-100 dark:hover:bg-gray-600 focus:z-10',
636-
option === 'SMB'
636+
option === 'Pro'
637637
? 'rounded-l-md'
638638
: option === 'Enterprise'
639639
? 'rounded-r-md -ml-px'
@@ -660,16 +660,16 @@ export default function Pricing() {
660660
__html:
661661
tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Nonprofit'
662662
? tier.description_nonprofit // Display the nonprofit description with links
663-
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
664-
? tier.description_smb // Display the SMB description with links
663+
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
664+
? tier.description_smb // Display the Pro description with links
665665
: tier.description // Default description with links
666666
}}
667667
/>
668668
</div>
669669

670670
<a
671671
href={
672-
tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
672+
tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
673673
? '#pro-plan'
674674
: tier.href
675675
}
@@ -678,15 +678,15 @@ export default function Pricing() {
678678
className={classNames(
679679
tier.id === 'tier-team'
680680
? 'bg-blue-600 !text-white shadow-sm hover:bg-blue-700'
681-
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
681+
: tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
682682
? 'bg-blue-600 !text-white shadow-sm hover:bg-blue-700'
683683
: tier.enterprise_edition
684684
? 'bg-teal-600 !text-white shadow-sm hover:bg-teal-700'
685685
: 'text-gray-900 hover:text-blue-600 dark:hover:text-blue-400 ring-1 ring-inset ring-gray-200 dark:ring-gray-600 hover:ring-gray-300 dark:hover:ring-gray-500 dark:text-white',
686686
'!no-underline mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-600'
687687
)}
688688
>
689-
{tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB'
689+
{tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro'
690690
? 'Check if you qualify'
691691
: tier.customMessage
692692
? tier.customMessage
@@ -698,7 +698,7 @@ export default function Pricing() {
698698
className="mt-8 space-y-3 text-sm leading-6 text-gray-600 xl:mt-10"
699699
style={{ marginBottom: '4rem' }}
700700
>
701-
{tier.id === 'tier-enterprise-selfhost' && selectedOption === 'SMB' ? (
701+
{tier.id === 'tier-enterprise-selfhost' && selectedOption === 'Pro' ? (
702702
<FeatureList features={tier.features_smb} level={1} id={tier.id} />
703703
) : (
704704
<FeatureList features={tier.features} level={1} id={tier.id} />
@@ -745,14 +745,14 @@ export default function Pricing() {
745745
onClick={(e) => {
746746
e.preventDefault();
747747
handlePlanClick(
748-
selectedOption === 'SMB'
748+
selectedOption === 'Pro'
749749
? 'https://billing.windmill.dev/b/28o3dq51Y6ZJ9jy7sM'
750750
: 'https://billing.windmill.dev/b/4gw4hu51YbfZ0N200j?prefilled_promo_code=nonprofit',
751751
selectedOption
752752
);
753753
}}
754754
className={classNames(
755-
selectedOption === 'SMB'
755+
selectedOption === 'Pro'
756756
? 'text-sm bg-blue-600 !text-white shadow-sm hover:bg-blue-700 focus-visible:outline-blue-600'
757757
: 'text-sm bg-teal-600 !text-white shadow-sm hover:bg-teal-700 focus-visible:outline-teal-600',
758758
'!no-underline text-center mt-6 block rounded-md py-2 px-3 font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'

src/components/QuoteForm.tsx

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ const DetailRow = ({ label, value, isSubItem, calculation, className = '' }: Det
2929
</div>
3030
);
3131

32+
type Workers = {
33+
native: number;
34+
} & (
35+
| {
36+
workerGroups: Array<{ workers: number; memoryGB: number }>; // For cloud
37+
}
38+
| {
39+
small: number;
40+
standard: number;
41+
large: number; // For self-hosted
42+
}
43+
);
44+
3245
export function QuoteForm({
3346
workers,
3447
developers,
@@ -40,19 +53,15 @@ export function QuoteForm({
4053
setOpen,
4154
selectedOption
4255
}: {
43-
workers: {
44-
native: number;
45-
small: number;
46-
standard: number;
47-
large: number;
48-
};
56+
workers: Workers;
4957
developers: number;
5058
operators: number;
5159
frequency: string;
5260
plan: 'selfhosted_ee' | 'cloud_ee';
5361
open: boolean;
5462
setOpen: (open: boolean) => void;
5563
selectedOption: string;
64+
total_price: number;
5665
}) {
5766
const [companyName, setCompanyName] = useState('');
5867
const [email, setEmail] = useState('');
@@ -91,7 +100,13 @@ export function QuoteForm({
91100
}
92101

93102
// Calculate total compute units with minimum of 2
94-
const computeUnits = Math.max(2, Math.ceil(workers.small / 2) + workers.standard + workers.native + (2 * workers.large));
103+
const computeUnits = plan === 'cloud_ee'
104+
? Math.max(2, (workers as { workerGroups: Array<{ workers: number; memoryGB: number }> })
105+
.workerGroups.reduce((sum, group) => sum + (group.memoryGB/2 * group.workers), 0) + workers.native)
106+
: Math.max(2, Math.ceil((workers as { small: number }).small / 2) +
107+
(workers as { standard: number }).standard +
108+
workers.native +
109+
(2 * (workers as { large: number }).large));
95110

96111
const seats = developers + Math.ceil(operators / 2);
97112

@@ -137,8 +152,16 @@ export function QuoteForm({
137152
}
138153
}
139154

140-
// Add this calculation before the return statement
141-
const rawComputeUnits = Math.ceil(workers.small / 2) + workers.standard + workers.native + (2 * workers.large);
155+
// Update this calculation before the return statement
156+
const rawComputeUnits = plan === 'cloud_ee'
157+
? (workers as { workerGroups: Array<{ workers: number; memoryGB: number }> }).workerGroups.reduce(
158+
(sum, group) => sum + (group.memoryGB/2 * group.workers), 0
159+
) + workers.native
160+
: Math.ceil((workers as { small: number; standard: number; large: number }).small / 2) +
161+
(workers as { small: number; standard: number; large: number }).standard +
162+
workers.native +
163+
(2 * (workers as { small: number; standard: number; large: number }).large);
164+
142165
const computeUnits = Math.max(2, rawComputeUnits);
143166
const isSmbWithTooManyUnits = selectedOption === 'SMB' && computeUnits > 10;
144167

@@ -211,25 +234,55 @@ export function QuoteForm({
211234
/>
212235

213236
{/* Worker type rows */}
214-
{Object.entries({
215-
'Standard workers': { count: workers.standard, multiplier: 1 },
216-
'Small workers': { count: workers.small, multiplier: 0.5 },
217-
'Large workers': { count: workers.large, multiplier: 2 },
218-
'Native workers': { count: workers.native, multiplier: 1, displayMultiplier: 8 }
219-
}).map(([label, { count, multiplier, displayMultiplier }]) =>
220-
count > 0 && (
221-
<DetailRow
222-
key={label}
223-
label={label}
224-
isSubItem
225-
calculation={{
226-
left: label === 'Native workers' ? count * (displayMultiplier || 1) : count,
227-
right: `${Math.ceil(count * multiplier)} CU`
228-
}}
229-
/>
237+
{plan === 'cloud_ee' ? (
238+
// For cloud, show workers with their memory sizes
239+
(workers as { workerGroups: Array<{ workers: number; memoryGB: number }> }).workerGroups.map((group, index) =>
240+
group.workers > 0 && (
241+
<DetailRow
242+
key={index}
243+
label={`${group.memoryGB}GB workers`}
244+
isSubItem
245+
calculation={{
246+
left: group.workers,
247+
right: `${(group.memoryGB/2 * group.workers)} CU`
248+
}}
249+
/>
250+
)
251+
)
252+
) : (
253+
// For self-hosted, show the original breakdown
254+
Object.entries({
255+
'Standard workers': { count: (workers as { standard: number }).standard, multiplier: 1 },
256+
'Small workers': { count: (workers as { small: number }).small, multiplier: 0.5 },
257+
'Large workers': { count: (workers as { large: number }).large, multiplier: 2 }
258+
}).map(([label, { count, multiplier }]) =>
259+
count > 0 && (
260+
<DetailRow
261+
key={label}
262+
label={label}
263+
isSubItem
264+
calculation={{
265+
left: count,
266+
right: `${Math.ceil(count * multiplier)} CU`
267+
}}
268+
/>
269+
)
230270
)
231271
)}
232272

273+
{/* Show native workers for both plans */}
274+
{workers.native > 0 && (
275+
<DetailRow
276+
key="Native workers"
277+
label="Native workers"
278+
isSubItem
279+
calculation={{
280+
left: workers.native * 8,
281+
right: `${workers.native} CU`
282+
}}
283+
/>
284+
)}
285+
233286
{plan === 'selfhosted_ee' && (selectedOption === 'Nonprofit' || selectedOption === 'SMB') && (
234287
<label className="flex flex-col">
235288
<span className="font-medium text-gray-800 dark:text-gray-200 text-sm">

0 commit comments

Comments
 (0)