fix: centralize compute disk limits data and update documentation (#41242)

https://linear.app/supabase/issue/FE-2226/fix-incorrect-baseline-and-max-iopsthroughput-values

- Centralized compute/disk limits in
packages/shared-data/compute-disk-limits.ts (MB/s, baseline+max
IOPS/throughput) and re-exported from shared-data.
- Hooked Docs compute/disk table into shared data, converted docs
text/table to MB/s baseline/max.
- Removed duplicated compute IOPS/throughput constants from Studio,
Studio now imports shared data.
- Updated Studio disk schema: compute-aware MAX IOPS/throughput
validation (MB/s), defaults computeSize to ci_micro, and gp3/io2 checks
use compute caps.
- Clarified disk update mutation to send throughput as MB/s despite
backend throughput_mbps field name.
- Added tests for compute-size mappings and IOPS helper logic.
- Added Infra ownership for shared compute/disk data in CODEOWNERS.
- Locked zod version via catalog and added zod dep to shared-data.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a reusable Compute Disk Limits table and centralized compute
disk limits dataset for dynamic display.

* **Documentation**
* Replaced static per-size tables with a component-driven MB/s view;
clarified baseline vs. burst behavior and updated guides and
troubleshooting.

* **Bug Fixes**
* Validation and UI guidance now honor compute-size limits for IOPS and
throughput.

* **Tests**
  * Expanded unit tests for sizing, mappings, and edge cases.

* **Chores**
* Published shared-data exports, added a validation schema, pinned a
dependency, and added ownership entries.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jordi Enric authored and GitHub committed 2026-02-03 10:52:42 +00:00
1 parent f0fbcbd2a3
commit 022b510269
19 files changed
+547 -245

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
packages/shared-data/compute-disk-limits.ts @supabase/infra
packages/shared-data/index.ts @supabase/infra
@@ -0,0 +1,34 @@
import {
COMPUTE_BASELINE_IOPS,
COMPUTE_BASELINE_THROUGHPUT,
COMPUTE_DISK,
COMPUTE_MAX_IOPS,
COMPUTE_MAX_THROUGHPUT,
} from 'shared-data'
export function ComputeDiskLimitsTable() {
return (
<table>
<thead>
<tr>
<th>Compute Instance</th>
<th>Baseline Throughput (MB/s)</th>
<th>Max Throughput (MB/s)</th>
<th>Baseline IOPS</th>
<th>Max IOPS</th>
</tr>
</thead>
<tbody>
{Object.entries(COMPUTE_DISK).map(([key, value]) => (
<tr key={key}>
<td>{value.name}</td>
<td>{COMPUTE_BASELINE_THROUGHPUT[key]?.toLocaleString()} MB/s</td>
<td>{COMPUTE_MAX_THROUGHPUT[key]?.toLocaleString()} MB/s</td>
<td>{COMPUTE_BASELINE_IOPS[key]?.toLocaleString()} IOPS</td>
<td>{COMPUTE_MAX_IOPS[key]?.toLocaleString()} IOPS</td>
</tr>
))}
</tbody>
</table>
)
}
@@ -83,23 +83,11 @@ The following sections explain how these attributes affect disk performance.
### Compute size
The compute size of your project sets the upper limit for disk throughput and IOPS. The table below shows the limits for each instance size. For instance, an 8XL compute instance has a maximum throughput of 9,500 Mbps and a maximum IOPS of 40,000.
The compute size of your project affects the effective disk throughput and IOPS. The table below shows both the baseline (sustained) limits and the burst (maximum) limits for each instance size. For instance, an 8XL compute instance has a throughput of 1,188 MB/s and IOPS of 40,000.
| Compute Instance | Disk Throughput | IOPS |
| ---------------- | --------------- | ----------- |
| Nano (free) | 43 Mbps | 250 IOPS |
| Micro | 87 Mbps | 500 IOPS |
| Small | 174 Mbps | 1,000 IOPS |
| Medium | 347 Mbps | 2,000 IOPS |
| Large | 630 Mbps | 3,600 IOPS |
| XL | 1,188 Mbps | 6,000 IOPS |
| 2XL | 2,375 Mbps | 12,000 IOPS |
| 4XL | 4,750 Mbps | 20,000 IOPS |
| 8XL | 9,500 Mbps | 40,000 IOPS |
| 12XL | 14,250 Mbps | 50,000 IOPS |
| 16XL | 19,000 Mbps | 80,000 IOPS |
<ComputeDiskLimitsTable />
Smaller compute instances like Nano, Micro, Small, and Medium have baseline performance levels that can occasionally be exceeded for short periods of time. If it does exceed the baseline, you should consider upgrading your instance size for a more reliable performance.
Smaller compute instances like Nano, Micro, Small, and Medium can burst above baseline for short periods of time. Once burst capacity is exhausted, performance returns to baseline. If you need consistent disk performance, consider upgrading your compute size.
Larger compute instances (4XL and above) are designed for sustained, high performance with specific IOPS and throughput limits which you can [configure](/docs/guides/platform/manage-your-usage/disk-throughput). If you hit your IOPS or throughput limit, throttling will occur.
@@ -7,7 +7,7 @@ database_id = "4844905d-1456-44a1-858e-7a4995e5054c"
## Understanding disk IO and disk IO budget
Disk IO refers to two metrics: throughput in Megabits per Second and IOPS which are Input/Output Operations per Second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size).
Disk IO refers to two metrics: throughput in Megabytes per second (MB/s) and IOPS which are Input/Output Operations per Second. Throughput measures how much data you can move each second, while IOPS measures how many read/write operations you can perform each second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size).
Smaller compute instances can burst and exceed their baseline performance for a short period of time every day. This is represented as your Disk IO Budget and once your Disk IO Budget is consumed, your instance reverts back to its baseline performance. Learn more about [choosing the right compute instance for consistent disk performance](/docs/guides/platform/compute-add-ons#choosing-the-right-compute-instance-for-consistent-disk-performance).
@@ -14,22 +14,11 @@ There are two primary values that matter for IO:
- **Disk Throughput**: how much data can be moved to and from disk per second
- **IOPS(Input/Output per second)**: how many read/write requests can be performed against your disk per second
Each compute instance has unique IO settings. The settings at the time this was written are listed below.
| Plan | Baseline Disk Throughput | Baseline IOPS |
|---------------|--------------------------|---------------|
| Nano (free) | 43 Mbps | 250 IOPS |
| Micro | 87 Mbps | 500 IOPS |
| Small | 174 Mbps | 1,000 IOPS |
| Medium | 347 Mbps | 2,000 IOPS |
| Large | 630 Mbps | 3,000 IOPS |
| XL | 1,048 Mbps | 3,000 IOPS |
| 2XL | 1,048 Mbps | 3,000 IOPS |
| 4XL | 1,048 Mbps | 3,000 IOPS |
| 8XL | 1,048 Mbps | 3,000 IOPS |
| 12XL | 1,048 Mbps | 3,000 IOPS |
| 16XL | 1,048 Mbps | 3,000 IOPS |
Each compute instance has unique IO settings. The current baseline (sustained) and max (burst) limits are listed below.
Compute sizes below XL, to accommodate their limitations, have burst budgets. This is when instances are allowed to utilize 1048 Mbps of disk throughput and 3,000 IOPS for 30 minutes before returning back to their baseline behavior.
<ComputeDiskLimitsTable />
Compute sizes below XL can burst above baseline for short periods before returning back to their baseline behavior.
There are other metrics that indicate IO strain.
@@ -19,6 +19,7 @@ import { NavData } from '~/components/NavData'
import { Price } from '~/components/Price'
import { ProjectConfigVariables } from '~/components/ProjectConfigVariables'
import { RealtimeLimitsEstimator } from '~/components/RealtimeLimitsEstimator'
import { ComputeDiskLimitsTable } from '~/components/ComputeDiskLimitsTable'
import { RegionsList, SmartRegionsList } from '~/components/RegionsList'
import { SharedData } from '~/components/SharedData'
import StepHikeCompact from '~/components/StepHikeCompact'
@@ -50,6 +51,7 @@ const components = {
ButtonCard,
CodeSampleDummy,
CodeSampleWrapper,
ComputeDiskLimitsTable,
CostWarning,
ErrorCodes,
Extensions,
@@ -1,24 +1,26 @@
import { CloudProvider } from 'shared-data'
import { CloudProvider, COMPUTE_MAX_IOPS, computeInstanceAddonVariantIdSchema } from 'shared-data'
import { z } from 'zod'
import { ComputeInstanceAddonVariantId } from './DiskManagement.types'
import {
calculateDiskSizeRequiredForIopsWithGp3,
calculateDiskSizeRequiredForIopsWithIo2,
calculateIopsRequiredForThroughput,
calculateMaxThroughput,
calculateMaxIopsAllowedForDiskSizeWithGp3,
calculateMaxIopsAllowedForDiskSizeWithio2,
formatNumber,
} from './DiskManagement.utils'
import { DISK_LIMITS, DiskType } from './ui/DiskManagement.constants'
import { COMPUTE_MAX_THROUGHPUT } from 'shared-data'
const baseSchema = z.object({
storageType: z.enum(['io2', 'gp3']).describe('Type of storage: io2 or gp3'),
totalSize: z.number().int('Value must be an integer').describe('Allocated disk size in GB'),
provisionedIOPS: z.number().describe('Provisioned IOPS for storage type'),
throughput: z.number().optional().describe('Throughput in MB/s for gp3'),
computeSize: z
.custom<ComputeInstanceAddonVariantId>((val): val is ComputeInstanceAddonVariantId => true)
.describe('Compute size'),
computeSize: computeInstanceAddonVariantIdSchema
.describe('Compute size')
.optional()
.default('ci_micro'),
growthPercent: z
.number()
.int('Value must be an integer')
@@ -55,7 +57,12 @@ export const CreateDiskStorageSchema = ({
const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject && !isAwsK8sProject
const schema = baseSchema.superRefine((data, ctx) => {
const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb } = data
const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb, computeSize } = data
const computeMaxIops = (() => {
const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
if (!parsedCompute.success) return Number.POSITIVE_INFINITY
return COMPUTE_MAX_IOPS[parsedCompute.data] ?? Number.POSITIVE_INFINITY
})()
if (validateDiskConfiguration && totalSize < 8) {
ctx.addIssue({
@@ -85,10 +92,11 @@ export const CreateDiskStorageSchema = ({
if (validateDiskConfiguration && storageType === 'io2') {
// Validation rules for io2
if (provisionedIOPS > DISK_LIMITS[DiskType.IO2].maxIops) {
const maxIopsForIo2 = Math.min(DISK_LIMITS[DiskType.IO2].maxIops, computeMaxIops)
if (provisionedIOPS > maxIopsForIo2) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `IOPS can not exceed ${formatNumber(DISK_LIMITS[DiskType.IO2].maxIops)} for io2 Disk type. Please reach out to support if you need higher IOPS than this.`,
message: `IOPS cannot exceed ${formatNumber(maxIopsForIo2)} for io2 Disk type and the selected compute size.`,
path: ['provisionedIOPS'],
})
}
@@ -141,11 +149,17 @@ export const CreateDiskStorageSchema = ({
if (validateDiskConfiguration && storageType === 'gp3') {
const maxIopsAllowedForDiskSizeWithGp3 = calculateMaxIopsAllowedForDiskSizeWithGp3(totalSize)
const maxIopsForGp3 = Math.min(DISK_LIMITS[DiskType.GP3].maxIops, computeMaxIops)
const computeMaxThroughput = (() => {
const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
if (!parsedCompute.success) return DISK_LIMITS[DiskType.GP3].maxThroughput
return COMPUTE_MAX_THROUGHPUT[parsedCompute.data] ?? DISK_LIMITS[DiskType.GP3].maxThroughput
})()
if (provisionedIOPS > DISK_LIMITS[DiskType.GP3].maxIops) {
if (provisionedIOPS > maxIopsForGp3) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `IOPS can not exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxIops)} for GP3 Disk. Change the Disk type to io2 for higher IOPS support.`,
message: `IOPS cannot exceed ${formatNumber(maxIopsForGp3)} for GP3 Disk and the selected compute size.`,
path: ['provisionedIOPS'],
})
}
@@ -176,16 +190,21 @@ export const CreateDiskStorageSchema = ({
}
if (throughput !== undefined) {
const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000)
if (throughput > DISK_LIMITS['gp3'].maxThroughput) {
// gp3 throughput scales with provisioned IOPS (capped by gp3 max)
const iopsThroughputLimit = calculateMaxThroughput(provisionedIOPS)
if (throughput > DISK_LIMITS[DiskType.GP3].maxThroughput) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Throughput can not exceed ${formatNumber(maxThroughput)} MB/s`,
message: `Throughput cannot exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxThroughput)} MB/s for GP3 disk type.`,
path: ['throughput'],
})
}
if (throughput > maxThroughput) {
} else if (throughput > computeMaxThroughput) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Throughput cannot exceed ${formatNumber(computeMaxThroughput)} MB/s for the selected compute size.`,
path: ['throughput'],
})
} else if (throughput > iopsThroughputLimit) {
const iopsRequiredForThroughput = calculateIopsRequiredForThroughput(throughput)
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -1,10 +1,70 @@
import { describe, test, expect } from 'vitest'
import { DiskType } from './ui/DiskManagement.constants'
import { describe, expect, test } from 'vitest'
import {
calculateBaselineIopsForComputeSize,
calculateComputeSizeRequiredForIops,
calculateDiskSizePrice,
calculateIOPSPrice,
calculateMaxIopsForComputeSize,
calculateThroughputPrice,
mapAddOnVariantIdToComputeSize,
mapComputeSizeNameToAddonVariantId,
} from './DiskManagement.utils'
import { DiskType } from './ui/DiskManagement.constants'
describe('DiskManagement utils', () => {
describe('mapComputeSizeNameToAddonVariantId', () => {
test('maps known infra sizes to addon variant ids', () => {
expect(mapComputeSizeNameToAddonVariantId('4xlarge')).toBe('ci_4xlarge')
})
test('falls back to nano for unknown infra sizes', () => {
// @ts-expect-error intentional invalid value for runtime guard
expect(mapComputeSizeNameToAddonVariantId('unknown-size')).toBe('ci_nano')
})
})
describe('mapAddOnVariantIdToComputeSize', () => {
test('maps known addon ids to display names', () => {
expect(mapAddOnVariantIdToComputeSize('ci_4xlarge')).toBe('4XL')
})
test('falls back to Nano on invalid addon id', () => {
// @ts-expect-error intentional invalid value for runtime guard
expect(mapAddOnVariantIdToComputeSize('ci_invalid')).toBe('Nano')
})
})
describe('calculateBaselineIopsForComputeSize / calculateMaxIopsForComputeSize', () => {
test('returns 0 for invalid compute ids', () => {
expect(calculateBaselineIopsForComputeSize('invalid')).toBe(0)
expect(calculateMaxIopsForComputeSize('invalid')).toBe(0)
})
test('returns baseline and max for valid compute ids', () => {
expect(calculateBaselineIopsForComputeSize('ci_2xlarge')).toBe(12000)
expect(calculateMaxIopsForComputeSize('ci_2xlarge')).toBe(20000)
})
})
describe('calculateComputeSizeRequiredForIops', () => {
test('returns smallest size that satisfies requested IOPS', () => {
expect(calculateComputeSizeRequiredForIops(500)).toBe('ci_nano')
expect(calculateComputeSizeRequiredForIops(19000)).toBe('ci_large')
expect(calculateComputeSizeRequiredForIops(45000)).toBe('ci_12xlarge')
})
test('falls back to largest size when exceeding known max', () => {
const fallback = calculateComputeSizeRequiredForIops(500000)
expect([
'ci_48xlarge',
'ci_48xlarge_optimized_cpu',
'ci_48xlarge_optimized_memory',
'ci_48xlarge_high_memory',
]).toContain(fallback)
})
})
})
describe('DiskManagement.utils.ts:calculateDiskSizePrice', () => {
test('GP3 with 8GB to GP3 with 10GB for pro plan', () => {
@@ -1,18 +1,18 @@
import { ProjectDetail } from 'data/projects/project-detail-query'
import { PlanId, ProjectAddonVariantMeta } from 'data/subscriptions/types'
import { INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from 'lib/constants'
import {
COMPUTE_BASELINE_IOPS,
COMPUTE_MAX_IOPS,
computeInstanceAddonVariantIdSchema,
} from 'shared-data'
import {
ComputeInstanceAddonVariantId,
ComputeInstanceSize,
InfraInstanceSize,
} from './DiskManagement.types'
import {
COMPUTE_BASELINE_IOPS,
DISK_LIMITS,
DISK_PRICING,
DiskType,
PLAN_DETAILS,
} from './ui/DiskManagement.constants'
import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './ui/DiskManagement.constants'
// Included disk size only applies to primary, not replicas
export const calculateDiskSizePrice = ({
@@ -159,31 +159,86 @@ export const calculateThroughputPrice = ({
return { oldPrice: '0.00', newPrice: '0.00' }
}
export function getAvailableComputeOptions(availableAddons: any[], projectCloudProvider?: string) {
const computeOptions =
availableAddons
.find((addon) => addon.type === 'compute_instance')
?.variants.filter((option: { [key: string]: any }) => {
if (!projectCloudProvider) {
return true
}
export type ComputeAddonVariant = {
identifier: ComputeInstanceAddonVariantId
name: string
price_description: string
price: number
price_interval: 'hourly' | 'monthly'
price_type: string
meta?: ProjectAddonVariantMeta
}
const meta = option.meta as ProjectAddonVariantMeta
type AvailableAddon = {
type: string
variants: Array<{
identifier: string
name: string
price_description: string
price: number
price_interval: 'hourly' | 'monthly'
price_type: string
meta?: unknown
}>
}
return (
!meta.supported_cloud_providers ||
meta.supported_cloud_providers.includes(projectCloudProvider)
)
}) ?? []
const isProjectAddonVariantMeta = (meta: unknown): meta is ProjectAddonVariantMeta => {
if (typeof meta !== 'object' || meta === null) return false
const obj = meta as Record<string, unknown>
// Validate supported_cloud_providers is an array if present (used at line 200)
if ('supported_cloud_providers' in obj && !Array.isArray(obj.supported_cloud_providers)) {
return false
}
// Check for at least one expected property to ensure it's likely a real ProjectAddonVariantMeta
const hasExpectedProperty =
'cpu_cores' in obj ||
'memory_gb' in obj ||
'cpu_dedicated' in obj ||
'baseline_disk_io_mbs' in obj ||
'max_disk_io_mbs' in obj ||
'connections_direct' in obj ||
'connections_pooler' in obj ||
'backup_duration_days' in obj ||
'supported_cloud_providers' in obj
return hasExpectedProperty
}
export function getAvailableComputeOptions(
availableAddons: AvailableAddon[],
projectCloudProvider?: string
) {
const computeAddon = availableAddons.find((addon) => addon.type === 'compute_instance')
const computeOptions: ComputeAddonVariant[] =
computeAddon?.variants.flatMap((option) => {
const parsedId = computeInstanceAddonVariantIdSchema.safeParse(option.identifier)
if (!parsedId.success) return []
if (projectCloudProvider && isProjectAddonVariantMeta(option.meta)) {
const isSupported =
!option.meta.supported_cloud_providers ||
option.meta.supported_cloud_providers.includes(projectCloudProvider)
if (!isSupported) return []
}
return [
{
...option,
identifier: parsedId.data,
meta: isProjectAddonVariantMeta(option.meta) ? option.meta : undefined,
},
]
}) ?? []
function hasMicroOptionFromApi() {
return (
availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? []
).some((variant: any) => variant.identifier === 'ci_micro')
return (computeAddon?.variants ?? []).some((variant) => variant.identifier === 'ci_micro')
}
// Backwards comp until API is deployed
if (!hasMicroOptionFromApi) {
if (!hasMicroOptionFromApi()) {
// Unshift to push to start of array
computeOptions.unshift({
identifier: 'ci_micro',
@@ -250,22 +305,42 @@ export const calculateIopsRequiredForThroughput = (throughput: number) => {
return Math.max(125, Math.ceil(throughput / 0.256))
}
export const calculateMaxIopsAllowedForComputeSize = (computeSize: string): number => {
return COMPUTE_BASELINE_IOPS[computeSize as keyof typeof COMPUTE_BASELINE_IOPS] || 0
export const calculateBaselineIopsForComputeSize = (computeSize: string): number => {
const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
if (!parsed.success) return 0
return COMPUTE_BASELINE_IOPS[parsed.data] ?? 0
}
export const calculateMaxIopsForComputeSize = (computeSize: string): number => {
const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize)
if (!parsed.success) return 0
return COMPUTE_MAX_IOPS[parsed.data] ?? 0
}
export const calculateComputeSizeRequiredForIops = (
iops: number
): ComputeInstanceAddonVariantId | undefined => {
const computeSizes = Object.entries(COMPUTE_BASELINE_IOPS).sort((a, b) => a[1] - b[1])
for (const [size, baselineIops] of computeSizes) {
if (iops <= baselineIops) {
return size as ComputeInstanceAddonVariantId
type ComputeSizeMax = { size: ComputeInstanceAddonVariantId; maxIops: number }
const computeSizes: ComputeSizeMax[] = Object.entries(COMPUTE_MAX_IOPS)
.map((entry) => {
const [size, maxIops] = entry
const parsedSize = computeInstanceAddonVariantIdSchema.safeParse(size)
if (!parsedSize.success) return undefined
return { size: parsedSize.data, maxIops: Number(maxIops) }
})
.filter((value): value is ComputeSizeMax => value !== undefined)
.sort((a, b) => a.maxIops - b.maxIops)
for (const { size, maxIops } of computeSizes) {
if (iops <= maxIops) {
return size
}
}
// fallback to largest compute size - this should never happen though :-/
return computeSizes[computeSizes.length - 1][0] as ComputeInstanceAddonVariantId
const fallbackSize = computeSizes[computeSizes.length - 1]?.size
if (!fallbackSize) return undefined
return fallbackSize
}
export const calculateDiskSizeRequiredForIops = (provisionedIOPS: number): number | undefined => {
@@ -291,57 +366,69 @@ export const formatComputeName = (compute: string) => {
return compute.toUpperCase().replace('CI_', '')
}
const infraToAddonVariant: Record<InfraInstanceSize, ComputeInstanceAddonVariantId> = {
pico: 'ci_nano',
nano: 'ci_nano',
micro: 'ci_micro',
small: 'ci_small',
medium: 'ci_medium',
large: 'ci_large',
xlarge: 'ci_xlarge',
'2xlarge': 'ci_2xlarge',
'4xlarge': 'ci_4xlarge',
'8xlarge': 'ci_8xlarge',
'12xlarge': 'ci_12xlarge',
'16xlarge': 'ci_16xlarge',
'24xlarge': 'ci_24xlarge',
'24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory',
'24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu',
'24xlarge_high_memory': 'ci_24xlarge_high_memory',
'48xlarge': 'ci_48xlarge',
'48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory',
'48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu',
'48xlarge_high_memory': 'ci_48xlarge_high_memory',
}
const isInfraInstanceSize = (value: string): value is InfraInstanceSize =>
Object.prototype.hasOwnProperty.call(infraToAddonVariant, value)
export const mapComputeSizeNameToAddonVariantId = (
computeSize: ProjectDetail['infra_compute_size']
): ComputeInstanceAddonVariantId => {
return {
pico: 'ci_pico',
nano: 'ci_nano',
micro: 'ci_micro',
small: 'ci_small',
medium: 'ci_medium',
large: 'ci_large',
xlarge: 'ci_xlarge',
'2xlarge': 'ci_2xlarge',
'4xlarge': 'ci_4xlarge',
'8xlarge': 'ci_8xlarge',
'12xlarge': 'ci_12xlarge',
'16xlarge': 'ci_16xlarge',
'24xlarge': 'ci_24xlarge',
'24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory',
'24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu',
'24xlarge_high_memory': 'ci_24xlarge_high_memory',
'48xlarge': 'ci_48xlarge',
'48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory',
'48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu',
'48xlarge_high_memory': 'ci_48xlarge_high_memory',
}[computeSize ?? 'nano'] as ComputeInstanceAddonVariantId
const fallback: InfraInstanceSize = 'nano'
const matchedSize = computeSize && isInfraInstanceSize(computeSize) ? computeSize : undefined
const sizeKey = matchedSize ?? fallback
return infraToAddonVariant[sizeKey]
}
const addonVariantToComputeSize: Record<ComputeInstanceAddonVariantId, ComputeInstanceSize> = {
ci_nano: 'Nano',
ci_micro: 'Micro',
ci_small: 'Small',
ci_medium: 'Medium',
ci_large: 'Large',
ci_xlarge: 'XL',
ci_2xlarge: '2XL',
ci_4xlarge: '4XL',
ci_8xlarge: '8XL',
ci_12xlarge: '12XL',
ci_16xlarge: '16XL',
ci_24xlarge: '24XL',
ci_24xlarge_optimized_memory: '24XL - Optimized Memory',
ci_24xlarge_optimized_cpu: '24XL - Optimized CPU',
ci_24xlarge_high_memory: '24XL - High Memory',
ci_48xlarge: '48XL',
ci_48xlarge_optimized_memory: '48XL - Optimized Memory',
ci_48xlarge_optimized_cpu: '48XL - Optimized CPU',
ci_48xlarge_high_memory: '48XL - High Memory',
}
export const mapAddOnVariantIdToComputeSize = (
addonVariantId: ComputeInstanceAddonVariantId = 'ci_nano'
): ComputeInstanceSize => {
return {
ci_nano: 'Nano',
ci_micro: 'Micro',
ci_small: 'Small',
ci_medium: 'Medium',
ci_large: 'Large',
ci_xlarge: 'XL',
ci_2xlarge: '2XL',
ci_4xlarge: '4XL',
ci_8xlarge: '8XL',
ci_12xlarge: '12XL',
ci_16xlarge: '16XL',
ci_24xlarge: '24XL',
ci_24xlarge_optimized_memory: '24XL - Optimized Memory',
ci_24xlarge_optimized_cpu: '24XL - Optimized CPU',
ci_24xlarge_high_memory: '24XL - High Memory',
ci_48xlarge: '48XL',
ci_48xlarge_optimized_memory: '48XL - Optimized Memory',
ci_48xlarge_optimized_cpu: '48XL - Optimized CPU',
ci_48xlarge_high_memory: '48XL - High Memory',
}[addonVariantId] as ComputeInstanceSize
const parsed = computeInstanceAddonVariantIdSchema.safeParse(addonVariantId)
if (!parsed.success) return addonVariantToComputeSize.ci_nano
return addonVariantToComputeSize[parsed.data]
}
export const formatNumber = (num: number): string => {
@@ -140,7 +140,7 @@ export function DiskManagementForm() {
provisionedIOPS: iops,
throughput: throughput_mbps,
totalSize: size_gb,
computeSize,
computeSize: computeSize ?? 'ci_micro',
growthPercent: growth_percent,
minIncrementGb: min_increment_gb,
maxSizeGb: max_size_gb,
@@ -167,6 +167,7 @@ export function DiskManagementForm() {
provisionedIOPS: iops,
throughput: throughput_mbps,
totalSize: size_gb,
computeSize: form.getValues('computeSize'),
}
if (!('requested_modification' in data)) {
@@ -212,7 +213,9 @@ export function DiskManagementForm() {
const usedPercentage = (usedSize / totalSize) * 100
const disableIopsThroughputConfig =
!isSpendCapEnabled && RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
modifiedComputeSize &&
!isSpendCapEnabled &&
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
const isBranch = project?.parent_project_ref !== undefined
@@ -469,7 +472,7 @@ export function DiskManagementForm() {
<div className="px-[var(--card-padding-x)] flex flex-col gap-y-8">
<NoticeBar
type="default"
visible={disableIopsThroughputConfig}
visible={!!disableIopsThroughputConfig}
title="Adjusting disk configuration requires LARGE Compute size or above"
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
actions={
@@ -28,7 +28,11 @@ import { ComputeBadge } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { ComputeInstanceAddonVariantId, InfraInstanceSize } from '../DiskManagement.types'
import { calculateComputeSizePrice, getAvailableComputeOptions } from '../DiskManagement.utils'
import {
calculateComputeSizePrice,
getAvailableComputeOptions,
ComputeAddonVariant,
} from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import FormMessage from '../ui/FormMessage'
import { NoticeBar } from '../ui/NoticeBar'
@@ -36,14 +40,6 @@ import { NoticeBar } from '../ui/NoticeBar'
/**
* to do: this could be a type from api-types
*/
type ComputeOption = {
identifier: ComputeInstanceAddonVariantId
name: string
price: number
price_interval: 'monthly' | 'hourly'
meta?: InstanceSpecs
}
type ComputeSizeFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
disabled?: boolean
@@ -170,7 +166,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
</FormMessage>
) : (
<>
{availableOptions.map((compute: ComputeOption) => {
{availableOptions.map((compute) => {
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
const lockedMicroDueToPITR =
@@ -187,10 +183,21 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
project?.infra_compute_size === 'nano' &&
compute.identifier === 'ci_nano'
? availableOptions.find(
(option: ComputeOption) => option.identifier === 'ci_micro'
(option: ComputeAddonVariant) => option.identifier === 'ci_micro'
)?.price
: compute.price
const cpuLabel = (() => {
const cpuCores = compute.meta?.cpu_cores
if (typeof cpuCores === 'number') {
return `${cpuCores}-core ${cpuArchitecture} CPU`
}
if (cpuCores) {
return `${cpuCores} CPU`
}
return 'CPU'
})()
return (
<RadioGroupCardItem
showIndicator={false}
@@ -263,12 +270,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {
size={14}
className="text-foreground-lighter"
/>
<span>
{compute.meta?.cpu_cores ?? 0}
{compute.meta?.cpu_cores !== 'Shared' &&
`-core ${cpuArchitecture}`}{' '}
CPU
</span>
<span>{cpuLabel}</span>
</div>
</div>
</div>
@@ -6,13 +6,11 @@ import { Admonition } from 'ui-patterns/admonition'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import {
calculateComputeSizeRequiredForIops,
calculateMaxIopsAllowedForComputeSize,
calculateMaxIopsForComputeSize,
mapAddOnVariantIdToComputeSize,
} from '../DiskManagement.utils'
import {
COMPUTE_BASELINE_IOPS,
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3,
} from './DiskManagement.constants'
import { COMPUTE_BASELINE_IOPS, COMPUTE_MAX_IOPS } from 'shared-data'
import { RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from './DiskManagement.constants'
interface ComputeSizeRecommendationSectionProps {
actions?: ReactNode
@@ -28,9 +26,10 @@ export function ComputeSizeRecommendationSection({
const iops = watch('provisionedIOPS')
const computeSizeRecommendedForIops = calculateComputeSizeRequiredForIops(iops)
const maxIOPSforComputeSize = calculateMaxIopsAllowedForComputeSize(computeSize ?? 'ci_micro')
const maxIopsForComputeSize = calculateMaxIopsForComputeSize(computeSize ?? 'ci_micro')
const isVisible =
iops > maxIOPSforComputeSize && !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize)
iops > maxIopsForComputeSize &&
!RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize ?? 'ci_micro')
return (
<AnimatePresence initial={false}>
@@ -43,12 +42,12 @@ export function ComputeSizeRecommendationSection({
>
<Admonition
type="default"
title={`Your Compute size can only support a baseline IOPS of ${COMPUTE_BASELINE_IOPS[computeSize]}`}
title={`Your compute size baseline is ${COMPUTE_BASELINE_IOPS[computeSize ?? 'ci_micro']} IOPS (max ${COMPUTE_MAX_IOPS[computeSize ?? 'ci_micro'] ?? maxIopsForComputeSize} IOPS)`}
>
<div className="flex flex-col gap-2">
<div>
<p className="text-sm text-foreground-light">
To achieve sustained IOPS performance we recommend using the{' '}
To achieve higher IOPS performance we recommend using the{' '}
<span className="text-foreground">
{mapAddOnVariantIdToComputeSize(computeSizeRecommendedForIops)}
</span>{' '}
@@ -60,93 +60,6 @@ export const PLAN_DETAILS: Record<PlanId, PlanDetails> = {
platform: { includedDiskGB: { gp3: 8, io2: 0 } },
}
export const COMPUTE_BASELINE_IOPS = {
ci_nano: 250,
ci_micro: 500,
ci_small: 1000,
ci_medium: 2000,
ci_large: 3600,
ci_xlarge: 6000,
ci_2xlarge: 12000,
ci_4xlarge: 20000,
ci_8xlarge: 40000,
ci_12xlarge: 50000,
ci_16xlarge: 80000,
ci_24xlarge: 120000,
ci_24xlarge_optimized_cpu: 120000,
ci_24xlarge_optimized_memory: 120000,
ci_24xlarge_high_memory: 120000,
ci_48xlarge: 240000,
ci_48xlarge_optimized_cpu: 240000,
ci_48xlarge_optimized_memory: 240000,
ci_48xlarge_high_memory: 240000,
}
export const COMPUTE_MAX_IOPS = {
ci_nano: 11800,
ci_micro: 11800,
ci_small: 11800,
ci_medium: 11800,
ci_large: 20000,
ci_xlarge: 20000,
ci_2xlarge: 20000,
ci_4xlarge: 20000,
ci_8xlarge: 40000,
ci_12xlarge: 50000,
ci_16xlarge: 80000,
ci_24xlarge: 120000,
ci_24xlarge_optimized_cpu: 120000,
ci_24xlarge_optimized_memory: 120000,
ci_24xlarge_high_memory: 120000,
ci_48xlarge: 240000,
ci_48xlarge_optimized_cpu: 240000,
ci_48xlarge_optimized_memory: 240000,
ci_48xlarge_high_memory: 240000,
}
export const COMPUTE_BASELINE_THROUGHPUT = {
ci_nano: 43,
ci_micro: 87,
ci_small: 174,
ci_medium: 347,
ci_large: 630,
ci_xlarge: 1188,
ci_2xlarge: 2375,
ci_4xlarge: 4750,
ci_8xlarge: 9500,
ci_12xlarge: 14250,
ci_16xlarge: 19000,
ci_24xlarge: 30000,
ci_24xlarge_optimized_cpu: 30000,
ci_24xlarge_optimized_memory: 30000,
ci_24xlarge_high_memory: 30000,
ci_48xlarge: 40000,
ci_48xlarge_optimized_cpu: 40000,
ci_48xlarge_optimized_memory: 40000,
ci_48xlarge_high_memory: 40000,
}
export const COMPUTE_MAX_THROUGHPUT = {
ci_nano: 2085,
ci_micro: 2085,
ci_small: 2085,
ci_medium: 2085,
ci_large: 4750,
ci_xlarge: 4750,
ci_2xlarge: 4750,
ci_4xlarge: 4750,
ci_8xlarge: 9500,
ci_12xlarge: 14250,
ci_16xlarge: 19000,
ci_24xlarge: 30000,
ci_24xlarge_optimized_cpu: 30000,
ci_24xlarge_optimized_memory: 30000,
ci_24xlarge_high_memory: 30000,
ci_48xlarge: 40000,
ci_48xlarge_optimized_cpu: 40000,
ci_48xlarge_optimized_memory: 40000,
ci_48xlarge_high_memory: 40000,
}
export const RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 = ['ci_nano', 'ci_micro', 'ci_small', 'ci_medium']
export const RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3 = [
@@ -11,7 +11,7 @@ export type UpdateDiskAttributesVariables = {
ref?: string
storageType: 'gp3' | 'io2'
provisionedIOPS: number
throughput?: number
throughput?: number // MB/s
totalSize: number
}
@@ -25,6 +25,8 @@ export async function updateDiskAttributes(updates: UpdateDiskAttributesVariable
updates.storageType === 'gp3'
? {
iops: updates.provisionedIOPS,
// API field name is throughput_mbps (legacy), but values are treated as MB/s in the platform API.
// If the platform switches to MiB/s, convert here before sending.
throughput_mbps: updates.throughput!,
size_gb: updates.totalSize,
type: updates.storageType,
+180
View File
@@ -0,0 +1,180 @@
import { z } from 'zod'
/**
* [Jordi] Use instances.vantage.sh as source of truth for compute disk limits.
* Eg: https://instances.vantage.sh/aws/ec2/t4g.nano?currency=USD
*
* All compute from medium down are t4g and the bigger ones are m6g.
*
* Throughput values are stored in MB/s (AWS lists Mbps, so we convert with toMBps).
*/
export type ComputeDiskLimit = {
name: string
baselineIops: number
maxIops: number
baselineThroughputMBps: number
maxThroughputMBps: number
}
const toMBps = (throughputMbps: number) => Math.round(throughputMbps / 8)
export const COMPUTE_DISK = {
ci_nano: {
name: 'Nano (free)',
baselineIops: 250,
maxIops: 11800,
baselineThroughputMBps: toMBps(43),
maxThroughputMBps: toMBps(2085),
},
ci_micro: {
name: 'Micro',
baselineIops: 500,
maxIops: 11800,
baselineThroughputMBps: toMBps(87),
maxThroughputMBps: toMBps(2085),
},
ci_small: {
name: 'Small',
baselineIops: 1000,
maxIops: 11800,
baselineThroughputMBps: toMBps(174),
maxThroughputMBps: toMBps(2085),
},
ci_medium: {
name: 'Medium',
baselineIops: 2000,
maxIops: 11800,
baselineThroughputMBps: toMBps(347),
maxThroughputMBps: toMBps(2085),
},
ci_large: {
name: 'Large',
baselineIops: 3600,
maxIops: 20000,
baselineThroughputMBps: toMBps(630),
maxThroughputMBps: toMBps(4750),
},
ci_xlarge: {
name: 'XL',
baselineIops: 6000,
maxIops: 20000,
baselineThroughputMBps: toMBps(1188),
maxThroughputMBps: toMBps(4750),
},
ci_2xlarge: {
name: '2XL',
baselineIops: 12000,
maxIops: 20000,
baselineThroughputMBps: toMBps(2375),
maxThroughputMBps: toMBps(4750),
},
ci_4xlarge: {
name: '4XL',
baselineIops: 20000,
maxIops: 20000,
baselineThroughputMBps: toMBps(4750),
maxThroughputMBps: toMBps(4750),
},
ci_8xlarge: {
name: '8XL',
baselineIops: 40000,
maxIops: 40000,
baselineThroughputMBps: toMBps(9500),
maxThroughputMBps: toMBps(9500),
},
ci_12xlarge: {
name: '12XL',
baselineIops: 50000,
maxIops: 50000,
baselineThroughputMBps: toMBps(14250),
maxThroughputMBps: toMBps(14250),
},
ci_16xlarge: {
name: '16XL',
baselineIops: 80000,
maxIops: 80000,
baselineThroughputMBps: toMBps(19000),
maxThroughputMBps: toMBps(19000),
},
ci_24xlarge: {
name: '24XL',
baselineIops: 120000,
maxIops: 120000,
baselineThroughputMBps: toMBps(30000),
maxThroughputMBps: toMBps(30000),
},
ci_24xlarge_optimized_cpu: {
name: '24XL - Optimized CPU',
baselineIops: 120000,
maxIops: 120000,
baselineThroughputMBps: toMBps(30000),
maxThroughputMBps: toMBps(30000),
},
ci_24xlarge_optimized_memory: {
name: '24XL - Optimized Memory',
baselineIops: 120000,
maxIops: 120000,
baselineThroughputMBps: toMBps(30000),
maxThroughputMBps: toMBps(30000),
},
ci_24xlarge_high_memory: {
name: '24XL - High Memory',
baselineIops: 120000,
maxIops: 120000,
baselineThroughputMBps: toMBps(30000),
maxThroughputMBps: toMBps(30000),
},
ci_48xlarge: {
name: '48XL',
baselineIops: 240000,
maxIops: 240000,
baselineThroughputMBps: toMBps(40000),
maxThroughputMBps: toMBps(40000),
},
ci_48xlarge_optimized_cpu: {
name: '48XL - Optimized CPU',
baselineIops: 240000,
maxIops: 240000,
baselineThroughputMBps: toMBps(40000),
maxThroughputMBps: toMBps(40000),
},
ci_48xlarge_optimized_memory: {
name: '48XL - Optimized Memory',
baselineIops: 240000,
maxIops: 240000,
baselineThroughputMBps: toMBps(40000),
maxThroughputMBps: toMBps(40000),
},
ci_48xlarge_high_memory: {
name: '48XL - High Memory',
baselineIops: 240000,
maxIops: 240000,
baselineThroughputMBps: toMBps(40000),
maxThroughputMBps: toMBps(40000),
},
} as const satisfies Record<string, ComputeDiskLimit>
export type ComputeDiskKey = keyof typeof COMPUTE_DISK
export const COMPUTE_BASELINE_IOPS = Object.fromEntries(
Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.baselineIops])
)
export const COMPUTE_MAX_IOPS = Object.fromEntries(
Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.maxIops])
)
export const COMPUTE_BASELINE_THROUGHPUT = Object.fromEntries(
Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.baselineThroughputMBps])
)
export const COMPUTE_MAX_THROUGHPUT = Object.fromEntries(
Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.maxThroughputMBps])
)
const computeInstanceAddonVariantIds = Object.keys(COMPUTE_DISK) as ComputeDiskKey[]
export const computeInstanceAddonVariantIdSchema = z.enum(
computeInstanceAddonVariantIds as [ComputeDiskKey, ...ComputeDiskKey[]]
)
+14
View File
@@ -1,4 +1,12 @@
import config from './config'
import {
COMPUTE_BASELINE_IOPS,
COMPUTE_BASELINE_THROUGHPUT,
COMPUTE_DISK,
COMPUTE_MAX_IOPS,
COMPUTE_MAX_THROUGHPUT,
computeInstanceAddonVariantIdSchema,
} from './compute-disk-limits'
import extensions from './extensions.json'
import logConstants from './logConstants'
import { plans, PricingInformation } from './plans'
@@ -11,6 +19,12 @@ import tweets, { topTweets } from './tweets'
export {
AWS_REGIONS,
COMPUTE_BASELINE_IOPS,
COMPUTE_BASELINE_THROUGHPUT,
COMPUTE_DISK,
COMPUTE_MAX_IOPS,
COMPUTE_MAX_THROUGHPUT,
computeInstanceAddonVariantIdSchema,
config,
extensions,
FLY_REGIONS,
+4 -1
View File
@@ -12,5 +12,8 @@
"tsconfig": "workspace:"
},
"author": "",
"license": "MIT"
"license": "MIT",
"dependencies": {
"zod": "catalog:"
}
}
+5 -1
View File
@@ -61,7 +61,7 @@ catalogs:
specifier: ^3.2.0
version: 3.2.4
zod:
specifier: ^3.25.76
specifier: 3.25.76
version: 3.25.76
overrides:
@@ -2257,6 +2257,10 @@ importers:
version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)
packages/shared-data:
dependencies:
zod:
specifier: 'catalog:'
version: 3.25.76
devDependencies:
tsconfig:
specifier: 'workspace:'
+1 -1
View File
@@ -25,7 +25,7 @@ catalog:
valtio: ^1.12.0
vite: ^7.1.11
vitest: ^3.2.0
zod: ^3.25.76
zod: 3.25.76
ignoredBuiltDependencies:
- '@parcel/watcher'