diff --git a/chains/manager.go b/chains/manager.go
index 977b62a14aa1..988b3ae5ba34 100644
--- a/chains/manager.go
+++ b/chains/manager.go
@@ -410,8 +410,8 @@ func (m *manager) buildChain(chainParams ChainParameters, sb Subnet) (*chain, er
 	// before it's first access would cause a panic.
 	ctx.SetState(snow.Initializing)
 
-	if sbConfigs, ok := m.SubnetConfigs[chainParams.SubnetID]; ok {
-		if sbConfigs.ValidatorOnly {
+	if subnetConfig, ok := m.SubnetConfigs[chainParams.SubnetID]; ok {
+		if subnetConfig.ValidatorOnly {
 			ctx.SetValidatorOnly()
 		}
 	}
@@ -450,11 +450,13 @@ func (m *manager) buildChain(chainParams ChainParameters, sb Subnet) (*chain, er
 	}
 
 	consensusParams := m.ConsensusParams
-	if sbConfigs, ok := m.SubnetConfigs[chainParams.SubnetID]; ok && chainParams.SubnetID != constants.PrimaryNetworkID {
-		consensusParams = sbConfigs.ConsensusParameters
+	// short circuit it before reading from subnetConfigs
+	if chainParams.SubnetID != constants.PrimaryNetworkID {
+		if subnetConfig, ok := m.SubnetConfigs[chainParams.SubnetID]; ok {
+			consensusParams = subnetConfig.ConsensusParameters
+		}
 	}
 
-	// The validators of this blockchain
 	var vdrs validators.Set // Validators validating this blockchain
 	var ok bool
 	if m.StakingEnabled {
@@ -569,8 +571,11 @@ func (m *manager) createAvalancheChain(
 	msgChan := make(chan common.Message, defaultChannelSize)
 
 	gossipConfig := m.GossipConfig
-	if sbConfigs, ok := m.SubnetConfigs[ctx.SubnetID]; ok && ctx.SubnetID != constants.PrimaryNetworkID {
-		gossipConfig = sbConfigs.GossipConfig
+	// short circuit it before reading from subnetConfigs
+	if ctx.SubnetID != constants.PrimaryNetworkID {
+		if subnetConfig, ok := m.SubnetConfigs[ctx.SubnetID]; ok {
+			gossipConfig = subnetConfig.GossipConfig
+		}
 	}
 
 	// Passes messages from the consensus engine to the network
@@ -757,8 +762,11 @@ func (m *manager) createSnowmanChain(
 	msgChan := make(chan common.Message, defaultChannelSize)
 
 	gossipConfig := m.GossipConfig
-	if sbConfigs, ok := m.SubnetConfigs[ctx.SubnetID]; ok && ctx.SubnetID != constants.PrimaryNetworkID {
-		gossipConfig = sbConfigs.GossipConfig
+	// short circuit it before reading from subnetConfigs
+	if ctx.SubnetID != constants.PrimaryNetworkID {
+		if subnetConfig, ok := m.SubnetConfigs[ctx.SubnetID]; ok {
+			gossipConfig = subnetConfig.GossipConfig
+		}
 	}
 
 	// Passes messages from the consensus engine to the network
diff --git a/config/config.go b/config/config.go
index 53b736e75bc6..5134d2ec3be3 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1007,11 +1007,7 @@ func getChainConfigsFromDir(v *viper.Viper) (map[string]chains.ChainConfig, erro
 		return make(map[string]chains.ChainConfig), nil
 	}
 
-	chainConfigs, err := readChainConfigPath(chainConfigPath)
-	if err != nil {
-		return nil, fmt.Errorf("couldn't read chain configs: %w", err)
-	}
-	return chainConfigs, nil
+	return readChainConfigPath(chainConfigPath)
 }
 
 // getChainConfigs reads & puts chainConfigs to node config
@@ -1060,6 +1056,13 @@ func readChainConfigPath(chainConfigPath string) (map[string]chains.ChainConfig,
 	return chainConfigMap, nil
 }
 
+func getSubnetConfigs(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]chains.SubnetConfig, error) {
+	if v.IsSet(SubnetConfigContentKey) {
+		return getSubnetConfigsFromFlags(v, subnetIDs)
+	}
+	return getSubnetConfigsFromDir(v, subnetIDs)
+}
+
 func getSubnetConfigsFromFlags(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]chains.SubnetConfig, error) {
 	subnetConfigContentB64 := v.GetString(SubnetConfigContentKey)
 	subnetConfigContent, err := base64.StdEncoding.DecodeString(subnetConfigContentB64)
@@ -1076,11 +1079,8 @@ func getSubnetConfigsFromFlags(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]c
 	res := make(map[ids.ID]chains.SubnetConfig)
 	for _, subnetID := range subnetIDs {
 		if rawSubnetConfigBytes, ok := subnetConfigs[subnetID]; ok {
-			subnetConfig := defaultSubnetConfig(v)
-			if err := json.Unmarshal(rawSubnetConfigBytes, &subnetConfig); err != nil {
-				return nil, err
-			}
-			if err := subnetConfig.ConsensusParameters.Valid(); err != nil {
+			subnetConfig, err := parseSubnetConfigs(rawSubnetConfigBytes, getDefaultSubnetConfig(v))
+			if err != nil {
 				return nil, err
 			}
 			res[subnetID] = subnetConfig
@@ -1100,22 +1100,7 @@ func getSubnetConfigsFromDir(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]cha
 		return make(map[ids.ID]chains.SubnetConfig), nil
 	}
 
-	subnetConfigs, err := readSubnetConfigs(subnetConfigPath, subnetIDs, defaultSubnetConfig(v))
-	if err != nil {
-		return nil, fmt.Errorf("couldn't read subnet configs: %w", err)
-	}
-	return subnetConfigs, nil
-}
-
-func getSubnetConfigs(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]chains.SubnetConfig, error) {
-	if v.IsSet(SubnetConfigContentKey) {
-		return getSubnetConfigsFromFlags(v, subnetIDs)
-	}
-	return getSubnetConfigsFromDir(v, subnetIDs)
-}
-
-// readSubnetConfigs reads subnet config files from a path and given subnetIDs and returns a map.
-func readSubnetConfigs(subnetConfigPath string, subnetIDs []ids.ID, defaultSubnetConfig chains.SubnetConfig) (map[ids.ID]chains.SubnetConfig, error) {
+	// reads subnet config files from a path and given subnetIDs and returns a map.
 	subnetConfigs := make(map[ids.ID]chains.SubnetConfig)
 	for _, subnetID := range subnetIDs {
 		filePath := filepath.Join(subnetConfigPath, subnetID.String()+subnetConfigFileExt)
@@ -1135,21 +1120,28 @@ func readSubnetConfigs(subnetConfigPath string, subnetIDs []ids.ID, defaultSubne
 		if err != nil {
 			return nil, err
 		}
-
-		configData := defaultSubnetConfig
-		if err := json.Unmarshal(file, &configData); err != nil {
-			return nil, err
-		}
-		if err := configData.ConsensusParameters.Valid(); err != nil {
+		config, err := parseSubnetConfigs(file, getDefaultSubnetConfig(v))
+		if err != nil {
 			return nil, err
 		}
-		subnetConfigs[subnetID] = configData
+		subnetConfigs[subnetID] = config
 	}
 
 	return subnetConfigs, nil
 }
 
-func defaultSubnetConfig(v *viper.Viper) chains.SubnetConfig {
+func parseSubnetConfigs(data []byte, defaultSubnetConfig chains.SubnetConfig) (chains.SubnetConfig, error) {
+	if err := json.Unmarshal(data, &defaultSubnetConfig); err != nil {
+		return chains.SubnetConfig{}, err
+	}
+
+	if err := defaultSubnetConfig.ConsensusParameters.Valid(); err != nil {
+		return chains.SubnetConfig{}, fmt.Errorf("invalid consensus parameters: %w", err)
+	}
+	return defaultSubnetConfig, nil
+}
+
+func getDefaultSubnetConfig(v *viper.Viper) chains.SubnetConfig {
 	return chains.SubnetConfig{
 		ConsensusParameters: getConsensusConfig(v),
 		ValidatorOnly:       false,
@@ -1341,14 +1333,24 @@ func GetNodeConfig(v *viper.Viper, buildDir string) (node.Config, error) {
 	// Subnet Configs
 	subnetConfigs, err := getSubnetConfigs(v, nodeConfig.WhitelistedSubnets.List())
 	if err != nil {
-		return node.Config{}, err
+		return node.Config{}, fmt.Errorf("couldn't read subnet configs: %w", err)
 	}
 	nodeConfig.SubnetConfigs = subnetConfigs
 
+	// Node health
+	nodeConfig.MinPercentConnectedStakeHealthy = map[ids.ID]float64{
+		constants.PrimaryNetworkID: calcMinConnectedStake(nodeConfig.ConsensusParams.Parameters),
+	}
+
+	nodeConfig.MinPercentConnectedStakeHealthy = make(map[ids.ID]float64)
+	for subnetID, config := range subnetConfigs {
+		nodeConfig.MinPercentConnectedStakeHealthy[subnetID] = calcMinConnectedStake(config.ConsensusParameters.Parameters)
+	}
+
 	// Chain Configs
 	nodeConfig.ChainConfigs, err = getChainConfigs(v)
 	if err != nil {
-		return node.Config{}, err
+		return node.Config{}, fmt.Errorf("couldn't read chain configs: %w", err)
 	}
 
 	// Profiler
@@ -1381,3 +1383,12 @@ func GetNodeConfig(v *viper.Viper, buildDir string) (node.Config, error) {
 	nodeConfig.DiskTargeterConfig, err = getDiskTargeterConfig(v)
 	return nodeConfig, err
 }
+
+// calcMinConnectedStake takes [consensusParams] as input and calculates the
+// expected min connected stake percentage according to alpha and k.
+func calcMinConnectedStake(consensusParams snowball.Parameters) float64 {
+	alpha := consensusParams.Alpha
+	k := consensusParams.K
+	r := float64(alpha) / float64(k)
+	return r*(1-constants.MinConnectedStakeBuffer) + constants.MinConnectedStakeBuffer
+}
diff --git a/config/config_test.go b/config/config_test.go
index 098da2e173bd..f8a48bbeb6f4 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -405,7 +405,7 @@ func TestGetSubnetConfigsFromFile(t *testing.T) {
 			testF: func(require *require.Assertions, given map[ids.ID]chains.SubnetConfig) {
 				require.Nil(given)
 			},
-			errMessage: "couldn't read subnet configs",
+			errMessage: "invalid character",
 		},
 		"subnet is not whitelisted": {
 			fileName:  "Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU.json",
@@ -578,6 +578,14 @@ func TestGetSubnetConfigsFromFlags(t *testing.T) {
 	}
 }
 
+func TestCalcMinConnectedStake(t *testing.T) {
+	v := setupViperFlags()
+	defaultParams := getConsensusConfig(v)
+	defaultExpectedMinStake := 0.8
+	minStake := calcMinConnectedStake(defaultParams.Parameters)
+	require.Equal(t, defaultExpectedMinStake, minStake)
+}
+
 // setups config json file and writes content
 func setupConfigJSON(t *testing.T, rootPath string, value string) string {
 	configFilePath := filepath.Join(rootPath, "config.json")
diff --git a/go.sum b/go.sum
index 3f0bfcabc7ab..6ac8a9418a88 100644
--- a/go.sum
+++ b/go.sum
@@ -57,8 +57,6 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/ava-labs/avalanche-network-runner-sdk v0.2.0 h1:YNvM0oFlb7A825kGe0XwwZuvIXTKF1BsuvxJdRLhIaI=
 github.com/ava-labs/avalanche-network-runner-sdk v0.2.0/go.mod h1:bEBRVZnGeRiNdDJAFUj+gA/TPzNDbpY/WzgDAHHwJb8=
-github.com/ava-labs/coreth v0.11.1-rc.0 h1:NeVdLi2wTu8EjX5jmaVbdOmzHOl3PQCs6RoBcziMyXU=
-github.com/ava-labs/coreth v0.11.1-rc.0/go.mod h1:jxBQyF3o5zMKJV1cnauJOcnPcvSEanvDXKUmcxlRAKE=
 github.com/ava-labs/coreth v0.11.1-rc.1 h1:GpSythfFCOXBoalsNvmzFBae0rgblTUnQuamgAVEhu4=
 github.com/ava-labs/coreth v0.11.1-rc.1/go.mod h1:8pN1Ko0RfPoEHzBVn9bzuv0uV1b+/vNTuYC+MqY8P0g=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
diff --git a/node/config.go b/node/config.go
index 3c90dd35bc0d..028a9d6a8a64 100644
--- a/node/config.go
+++ b/node/config.go
@@ -222,4 +222,7 @@ type Config struct {
 
 	RequiredAvailableDiskSpace         uint64 `json:"requiredAvailableDiskSpace"`
 	WarningThresholdAvailableDiskSpace uint64 `json:"warningThresholdAvailableDiskSpace"`
+
+	// See comment on [MinPercentConnectedStakeHealthy] in platformvm.Config
+	MinPercentConnectedStakeHealthy map[ids.ID]float64 `json:"minPercentConnectedStakeHealthy"`
 }
diff --git a/node/node.go b/node/node.go
index db1fe8139f26..d4f8d4330892 100644
--- a/node/node.go
+++ b/node/node.go
@@ -773,32 +773,33 @@ func (n *Node) initVMs() error {
 	errs.Add(
 		vmRegisterer.Register(constants.PlatformVMID, &platformvm.Factory{
 			Config: config.Config{
-				Chains:                        n.chainManager,
-				Validators:                    vdrs,
-				SubnetTracker:                 n.Net,
-				UptimeLockedCalculator:        n.uptimeCalculator,
-				StakingEnabled:                n.Config.EnableStaking,
-				WhitelistedSubnets:            n.Config.WhitelistedSubnets,
-				TxFee:                         n.Config.TxFee,
-				CreateAssetTxFee:              n.Config.CreateAssetTxFee,
-				CreateSubnetTxFee:             n.Config.CreateSubnetTxFee,
-				TransformSubnetTxFee:          n.Config.TransformSubnetTxFee,
-				CreateBlockchainTxFee:         n.Config.CreateBlockchainTxFee,
-				AddPrimaryNetworkValidatorFee: n.Config.AddPrimaryNetworkValidatorFee,
-				AddPrimaryNetworkDelegatorFee: n.Config.AddPrimaryNetworkDelegatorFee,
-				AddSubnetValidatorFee:         n.Config.AddSubnetValidatorFee,
-				AddSubnetDelegatorFee:         n.Config.AddSubnetDelegatorFee,
-				UptimePercentage:              n.Config.UptimeRequirement,
-				MinValidatorStake:             n.Config.MinValidatorStake,
-				MaxValidatorStake:             n.Config.MaxValidatorStake,
-				MinDelegatorStake:             n.Config.MinDelegatorStake,
-				MinDelegationFee:              n.Config.MinDelegationFee,
-				MinStakeDuration:              n.Config.MinStakeDuration,
-				MaxStakeDuration:              n.Config.MaxStakeDuration,
-				RewardConfig:                  n.Config.RewardConfig,
-				ApricotPhase3Time:             version.GetApricotPhase3Time(n.Config.NetworkID),
-				ApricotPhase5Time:             version.GetApricotPhase5Time(n.Config.NetworkID),
-				BanffTime:                     version.GetBanffTime(n.Config.NetworkID),
+				Chains:                          n.chainManager,
+				Validators:                      vdrs,
+				SubnetTracker:                   n.Net,
+				UptimeLockedCalculator:          n.uptimeCalculator,
+				StakingEnabled:                  n.Config.EnableStaking,
+				WhitelistedSubnets:              n.Config.WhitelistedSubnets,
+				TxFee:                           n.Config.TxFee,
+				CreateAssetTxFee:                n.Config.CreateAssetTxFee,
+				CreateSubnetTxFee:               n.Config.CreateSubnetTxFee,
+				TransformSubnetTxFee:            n.Config.TransformSubnetTxFee,
+				CreateBlockchainTxFee:           n.Config.CreateBlockchainTxFee,
+				AddPrimaryNetworkValidatorFee:   n.Config.AddPrimaryNetworkValidatorFee,
+				AddPrimaryNetworkDelegatorFee:   n.Config.AddPrimaryNetworkDelegatorFee,
+				AddSubnetValidatorFee:           n.Config.AddSubnetValidatorFee,
+				AddSubnetDelegatorFee:           n.Config.AddSubnetDelegatorFee,
+				UptimePercentage:                n.Config.UptimeRequirement,
+				MinValidatorStake:               n.Config.MinValidatorStake,
+				MaxValidatorStake:               n.Config.MaxValidatorStake,
+				MinDelegatorStake:               n.Config.MinDelegatorStake,
+				MinDelegationFee:                n.Config.MinDelegationFee,
+				MinStakeDuration:                n.Config.MinStakeDuration,
+				MaxStakeDuration:                n.Config.MaxStakeDuration,
+				RewardConfig:                    n.Config.RewardConfig,
+				ApricotPhase3Time:               version.GetApricotPhase3Time(n.Config.NetworkID),
+				ApricotPhase5Time:               version.GetApricotPhase5Time(n.Config.NetworkID),
+				BanffTime:                       version.GetBanffTime(n.Config.NetworkID),
+				MinPercentConnectedStakeHealthy: n.Config.MinPercentConnectedStakeHealthy,
 			},
 		}),
 		vmRegisterer.Register(constants.AVMID, &avm.Factory{
diff --git a/utils/constants/networking.go b/utils/constants/networking.go
index 7046efd6f857..28f45b4da505 100644
--- a/utils/constants/networking.go
+++ b/utils/constants/networking.go
@@ -25,4 +25,9 @@ const (
 	DefaultByteSliceCap    = 128
 
 	MaxContainersLen = int(4 * DefaultMaxMessageSize / 5)
+
+	// MinConnectedStakeBuffer is the safety buffer for calculation of MinConnectedStake.
+	// This increases the required stake percentage above alpha/k. Must be [0-1]
+	// 0 means MinConnectedStake = alpha/k, 1 means MinConnectedStake = 1 (fully connected)
+	MinConnectedStakeBuffer = .2
 )
diff --git a/vms/platformvm/config/config.go b/vms/platformvm/config/config.go
index f86cbf5032af..9533f14f5fb7 100644
--- a/vms/platformvm/config/config.go
+++ b/vms/platformvm/config/config.go
@@ -95,6 +95,15 @@ type Config struct {
 
 	// Time of the Banff network upgrade
 	BanffTime time.Time
+
+	// Subnet ID --> Minimum portion of the subnet's stake this node must be
+	// connected to in order to report healthy.
+	// [constants.PrimaryNetworkID] is always a key in this map.
+	// If a subnet is in this map, but it isn't whitelisted, its corresponding
+	// value isn't used.
+	// If a subnet is whitelisted but not in this map, we use the value for the
+	// Primary Network.
+	MinPercentConnectedStakeHealthy map[ids.ID]float64
 }
 
 func (c *Config) IsApricotPhase3Activated(timestamp time.Time) bool {
diff --git a/vms/platformvm/health.go b/vms/platformvm/health.go
index 249766a3ca10..ba6f4b75ffa2 100644
--- a/vms/platformvm/health.go
+++ b/vms/platformvm/health.go
@@ -4,15 +4,17 @@
 package platformvm
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 
 	"github.com/ava-labs/avalanchego/utils/constants"
+	"go.uber.org/zap"
 )
 
-// MinConnectedStake is the minimum percentage of the Primary Network's that
-// this node must be connected to to be considered healthy
-const MinConnectedStake = .80
+const fallbackMinPercentConnected = 0.8
+
+var errNotEnoughStake = errors.New("not connected to enough stake")
 
 func (vm *VM) HealthCheck() (interface{}, error) {
 	// Returns nil if this node is connected to > alpha percent of the Primary Network's stake
@@ -25,13 +27,24 @@ func (vm *VM) HealthCheck() (interface{}, error) {
 		"primary-percentConnected": primaryPercentConnected,
 	}
 
-	// TODO: Use alpha from consensus instead of const
+	primaryMinPercentConnected, ok := vm.MinPercentConnectedStakeHealthy[constants.PrimaryNetworkID]
+	if !ok {
+		// This should never happen according to the comment for
+		// [MinPercentConnectedStakeHealthy] but we include it here to avoid the
+		// situation where a regression causes the key to be missing so that we
+		// don't accidentally set [primaryMinPercentConnected] to 0.
+		vm.ctx.Log.Warn("primary network min connected stake not given",
+			zap.Float64("fallback value", fallbackMinPercentConnected),
+		)
+		primaryMinPercentConnected = fallbackMinPercentConnected
+	}
+
 	var errorReasons []string
-	if primaryPercentConnected < MinConnectedStake {
+	if primaryPercentConnected < primaryMinPercentConnected {
 		errorReasons = append(errorReasons,
 			fmt.Sprintf("connected to %f%% of primary network stake; should be connected to at least %f%%",
 				primaryPercentConnected*100,
-				MinConnectedStake*100,
+				primaryMinPercentConnected*100,
 			),
 		)
 	}
@@ -41,24 +54,28 @@ func (vm *VM) HealthCheck() (interface{}, error) {
 		if err != nil {
 			return nil, fmt.Errorf("couldn't get percent connected for %q: %w", subnetID, err)
 		}
+		minPercentConnected, ok := vm.MinPercentConnectedStakeHealthy[subnetID]
+		if !ok {
+			minPercentConnected = primaryMinPercentConnected
+		}
 
 		vm.metrics.SetSubnetPercentConnected(subnetID, percentConnected)
 		key := fmt.Sprintf("%s-percentConnected", subnetID)
 		details[key] = percentConnected
 
-		if percentConnected < MinConnectedStake {
+		if percentConnected < minPercentConnected {
 			errorReasons = append(errorReasons,
 				fmt.Sprintf("connected to %f%% of %q weight; should be connected to at least %f%%",
 					percentConnected*100,
 					subnetID,
-					MinConnectedStake*100,
+					minPercentConnected*100,
 				),
 			)
 		}
 	}
 
 	if len(errorReasons) > 0 {
-		err = fmt.Errorf("platform layer is unhealthy reason: %s", strings.Join(errorReasons, ", "))
+		err = fmt.Errorf("platform layer is unhealthy err: %w, details: %s", errNotEnoughStake, strings.Join(errorReasons, ", "))
 	}
 	return details, err
 }
diff --git a/vms/platformvm/health_test.go b/vms/platformvm/health_test.go
new file mode 100644
index 000000000000..95ab622a3b18
--- /dev/null
+++ b/vms/platformvm/health_test.go
@@ -0,0 +1,106 @@
+// Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package platformvm
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/ava-labs/avalanchego/ids"
+	"github.com/ava-labs/avalanchego/version"
+)
+
+const defaultMinConnectedStake = 0.8
+
+func TestHealthCheckPrimaryNetwork(t *testing.T) {
+	require := require.New(t)
+
+	vm, _, _ := defaultVM()
+	vm.ctx.Lock.Lock()
+
+	defer func() {
+		require.NoError(vm.Shutdown())
+		vm.ctx.Lock.Unlock()
+	}()
+	genesisState, _ := defaultGenesis()
+	for index, validator := range genesisState.Validators {
+		err := vm.Connected(validator.NodeID, version.CurrentApp)
+		require.NoError(err)
+		details, err := vm.HealthCheck()
+		if float64((index+1)*20) >= defaultMinConnectedStake*100 {
+			require.NoError(err)
+		} else {
+			require.Contains(details, "primary-percentConnected")
+			require.ErrorIs(err, errNotEnoughStake)
+		}
+	}
+}
+
+func TestHealthCheckSubnet(t *testing.T) {
+	tests := map[string]struct {
+		minStake   float64
+		useDefault bool
+	}{
+		"default min stake": {
+			useDefault: true,
+			minStake:   0,
+		},
+		"custom min stake": {
+			minStake: 0.40,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			require := require.New(t)
+
+			vm, _, _ := defaultVM()
+			vm.ctx.Lock.Lock()
+			defer func() {
+				require.NoError(vm.Shutdown())
+				vm.ctx.Lock.Unlock()
+			}()
+			subnetID := ids.GenerateTestID()
+			vm.WhitelistedSubnets.Add(subnetID)
+			testVdrCount := 4
+			for i := 0; i < testVdrCount; i++ {
+				subnetVal := ids.GenerateTestNodeID()
+				require.NoError(vm.Validators.AddWeight(subnetID, subnetVal, 100))
+			}
+
+			vals, ok := vm.Validators.GetValidators(subnetID)
+			require.True(ok)
+
+			// connect to all primary network validators first
+			genesisState, _ := defaultGenesis()
+			for _, validator := range genesisState.Validators {
+				err := vm.Connected(validator.NodeID, version.CurrentApp)
+				require.NoError(err)
+			}
+			var expectedMinStake float64
+			if test.useDefault {
+				expectedMinStake = defaultMinConnectedStake
+			} else {
+				expectedMinStake = test.minStake
+				vm.MinPercentConnectedStakeHealthy = map[ids.ID]float64{
+					subnetID: expectedMinStake,
+				}
+			}
+			for index, validator := range vals.List() {
+				err := vm.Connected(validator.ID(), version.CurrentApp)
+				require.NoError(err)
+				details, err := vm.HealthCheck()
+				connectedPerc := float64((index + 1) * (100 / testVdrCount))
+				if connectedPerc >= expectedMinStake*100 {
+					require.NoError(err)
+				} else {
+					require.Contains(details, fmt.Sprintf("%s-percentConnected", subnetID))
+					require.ErrorIs(err, errNotEnoughStake)
+				}
+			}
+		})
+	}
+}