Skip to content
GitHubXDiscordRSS

Health Check

Stop guessing if your origins are up. Cloudflare Health Checks ping your servers and report back. Pair them with Load Balancers to auto-route traffic away from unhealthy origins.

Get a basic health check running:

import { HealthCheck } from "alchemy/cloudflare";
const healthCheck = await HealthCheck("api-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "api.example.com",
name: "api-server-check"
});

This creates a health check that:

  • Performs HTTP checks every 60 seconds (default)
  • Marks the origin unhealthy after 1 consecutive failure (default)
  • Marks the origin healthy after 1 consecutive success (default)

Dial in your monitoring protocol; timing, response & validation:

import { HealthCheck } from "alchemy/cloudflare";
const healthCheck = await HealthCheck("backend-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353", // Zone ID or Zone resource
address: "backend.example.com", // Origin server hostname or IP
name: "backend-server-check", // Health check identifier
type: "HTTPS", // Protocol: "HTTP", "HTTPS", or "TCP"
// Timing configuration
interval: 30, // Check every 30 seconds
timeout: 10, // Timeout after 10 seconds
retries: 3, // Retry 3 times on timeout
// Threshold configuration
consecutiveFails: 2, // Mark unhealthy after 2 failures
consecutiveSuccesses: 2, // Mark healthy after 2 successes
// HTTP/HTTPS specific configuration
httpConfig: {
path: "/health", // Health check endpoint
method: "GET", // HTTP method: "GET" or "HEAD"
expectedCodes: ["200", "201"], // Expected status codes
expectedBody: "OK", // Expected response body substring
followRedirects: true, // Follow 3xx redirects
allowInsecure: false, // Validate SSL certificates
port: 443, // Port (defaults: 80 for HTTP, 443 for HTTPS)
header: { // Custom headers
"Host": ["backend.example.com"],
"X-Health-Check": ["true"]
}
},
// Monitoring configuration
checkRegions: ["WNAM", "ENAM", "WEU"], // Regions to check from
suspended: false, // Enable/disable checks
description: "Backend server health monitoring"
});

Monitor web servers and APIs with HTTP or HTTPS health checks:

const webHealthCheck = await HealthCheck("web-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "web.example.com",
name: "web-server-check",
type: "HTTP",
httpConfig: {
path: "/",
expectedCodes: ["200"]
}
});
const apiHealthCheck = await HealthCheck("api-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "api.example.com",
name: "api-endpoint-check",
type: "HTTPS",
httpConfig: {
path: "/api/health",
method: "GET",
expectedCodes: ["200", "201"],
expectedBody: "healthy",
header: {
"Host": ["api.example.com"],
"User-Agent": ["Cloudflare-Health-Check"],
"X-API-Key": [alchemy.secret.env.HEALTH_CHECK_API_KEY]
},
port: 443,
allowInsecure: false
}
});

Use ranges to match multiple status codes:

const flexibleHealthCheck = await HealthCheck("flexible-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "app.example.com",
name: "app-health-check",
httpConfig: {
path: "/health",
expectedCodes: ["2xx", "3xx"] // Accept any 2xx or 3xx response
}
});

For databases, mail servers, or anything that speaks TCP:

const databaseHealthCheck = await HealthCheck("db-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "db.example.com",
name: "database-connection-check",
type: "TCP",
tcpConfig: {
port: 5432, // PostgreSQL port
method: "connection_established"
},
interval: 60,
timeout: 5,
retries: 2
});

Common TCP ports:

  • PostgreSQL: 5432
  • MySQL: 3306
  • Redis: 6379
  • MongoDB: 27017
  • SMTP: 25, 587, 465
  • SSH: 22

Configure which regions run health checks for geographic distribution:

const globalHealthCheck = await HealthCheck("global-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "global.example.com",
name: "global-origin-check",
checkRegions: [
"WNAM", // Western North America
"ENAM", // Eastern North America
"WEU", // Western Europe
"EEU", // Eastern Europe
"SEAS", // South East Asia
"OC" // Oceania
]
});

Available regions:

  • WNAM - Western North America
  • ENAM - Eastern North America
  • WEU - Western Europe
  • EEU - Eastern Europe
  • NSAM - Northern South America
  • SSAM - Southern South America
  • OC - Oceania
  • ME - Middle East
  • NAF - Northern Africa
  • SAF - Southern Africa
  • IN - India
  • SEAS - South East Asia
  • NEAS - North East Asia
  • ALL_REGIONS - All available regions

Fine-tune health check timing and failure thresholds:

const sensitiveHealthCheck = await HealthCheck("sensitive-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "critical.example.com",
name: "critical-service-check",
// Check frequently for fast detection
interval: 15, // Check every 15 seconds
timeout: 3, // 3 second timeout
retries: 5, // Retry 5 times on timeout
// Conservative thresholds to avoid false positives
consecutiveFails: 3, // 3 failures before marking unhealthy
consecutiveSuccesses: 3, // 3 successes before marking healthy
httpConfig: {
path: "/health",
expectedCodes: ["200"]
}
});

Timing guidelines:

  • High availability services: Use shorter intervals (15-30s) with higher thresholds (2-3)
  • Normal services: Use default intervals (60s) with default thresholds (1-2)
  • Low priority services: Use longer intervals (120s+) with lower thresholds (1)

Calculation example: With interval: 30, consecutiveFails: 3:

  • Time to detect failure: 90 seconds (3 × 30s)
  • Time to recover: 90 seconds (3 × 30s)

Use secrets for sensitive header values:

const secureHealthCheck = await HealthCheck("secure-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "api.example.com",
name: "secure-api-check",
httpConfig: {
path: "/internal/health",
header: {
"Host": ["api.example.com"],
"Authorization": [alchemy.secret.env.HEALTH_CHECK_TOKEN],
"X-API-Key": [alchemy.secret.env.API_KEY]
}
}
});

Already have health checks? Adopt them by name instead of failing:

const existingHealthCheck = await HealthCheck("existing-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "existing.example.com",
name: "existing-health-check",
adopt: true // Adopt if health check with this name exists
});

Pause checks during maintenance without deleting:

const suspendedHealthCheck = await HealthCheck("maintenance-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "maintenance.example.com",
name: "maintenance-server-check",
suspended: true // Don't send health checks
});

When to suspend:

  • During planned maintenance
  • When origin is intentionally offline
  • When debugging health check configuration
  • When origin can’t handle health check load

Real power: automatic failover when origins go down.

import { HealthCheck, LoadBalancer, Pool } from "alchemy/cloudflare";
// Create health checks for each origin
const primaryHealthCheck = await HealthCheck("primary-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "primary.example.com",
name: "primary-origin-check",
httpConfig: {
path: "/health",
expectedCodes: ["200"]
}
});
const secondaryHealthCheck = await HealthCheck("secondary-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "secondary.example.com",
name: "secondary-origin-check",
httpConfig: {
path: "/health",
expectedCodes: ["200"]
}
});
// Create a pool with origins and health checks
const pool = await Pool("main-pool", {
name: "main-origin-pool",
origins: [
{
name: "primary",
address: "primary.example.com",
enabled: true
},
{
name: "secondary",
address: "secondary.example.com",
enabled: true
}
],
monitor: primaryHealthCheck // Attach health check to pool
});
// Create load balancer
const loadBalancer = await LoadBalancer("main-lb", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
name: "app.example.com",
defaultPools: [pool]
});

Access health check status programmatically:

const healthCheck = await HealthCheck("monitored-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "app.example.com",
name: "app-health-check"
});
// Health check status is available after creation
console.log(healthCheck.status); // "unknown" | "healthy" | "unhealthy" | "suspended"
console.log(healthCheck.failureReason); // Reason if unhealthy
console.log(healthCheck.createdOn); // Creation timestamp
console.log(healthCheck.modifiedOn); // Last modification timestamp

Status values:

  • unknown - Health check hasn’t run yet
  • healthy - Origin is responding correctly
  • unhealthy - Origin is not responding or failing checks
  • suspended - Health checks are suspended

Check all your geographic origins:

const regions = [
{ region: "us-west", address: "us-west.example.com" },
{ region: "us-east", address: "us-east.example.com" },
{ region: "eu-west", address: "eu-west.example.com" },
{ region: "ap-southeast", address: "ap-southeast.example.com" }
];
const healthChecks = await Promise.all(
regions.map(({ region, address }) =>
HealthCheck(`${region}-health`, {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address,
name: `${region}-origin-check`,
httpConfig: {
path: "/health",
expectedCodes: ["200"],
header: {
"X-Region": [region]
}
}
})
)
);

Check more than “is it up”, validate the whole stack:

const comprehensiveHealthCheck = await HealthCheck("comprehensive-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "api.example.com",
name: "comprehensive-check",
httpConfig: {
path: "/health/comprehensive",
method: "GET",
expectedCodes: ["200"],
expectedBody: "all_systems_operational", // Custom validation
header: {
"X-Health-Check-Version": ["v2"]
}
},
interval: 30,
timeout: 10,
consecutiveFails: 2,
consecutiveSuccesses: 2
});

Your health endpoint should validate:

  • Database connectivity
  • External service availability
  • Disk space
  • Memory usage
  • Critical background jobs

Fast to kill, slow to restore - avoid thundering herd:

const cautiousHealthCheck = await HealthCheck("cautious-health", {
zone: "023e105f4ecef8ad9ca31a8372d0c353",
address: "recovering.example.com",
name: "recovering-origin-check",
// Quick to mark unhealthy
consecutiveFails: 1,
// Slow to mark healthy (wait for stability)
consecutiveSuccesses: 5,
interval: 15,
httpConfig: {
path: "/health",
expectedCodes: ["200"]
}
});

This pattern:

  • ✅ Quickly removes unhealthy origins from rotation (1 failure)
  • ✅ Waits for sustained health before restoration (5 successes)
  • ✅ Reduces risk of cascading failures
  • ✅ Protects recovering origins from traffic spikes

Origin is up, but health check says it’s down:

Common causes:

  1. Incorrect path or port:
// ❌ Wrong path
httpConfig: {
path: "/healthcheck" // Origin expects "/health"
}
// ✅ Correct path
httpConfig: {
path: "/health"
}
  1. Missing or incorrect headers:
// ❌ Missing Host header
httpConfig: {
path: "/health"
}
// ✅ Include Host header
httpConfig: {
path: "/health",
header: {
"Host": ["api.example.com"]
}
}
  1. Firewall blocking health checks:
  • Ensure your firewall allows Cloudflare IP ranges
  • Check your origin’s access logs for health check requests
  • Verify security groups allow traffic from Cloudflare
  1. SSL certificate validation:
// ❌ Self-signed certificate with validation enabled
type: "HTTPS",
httpConfig: {
allowInsecure: false
}
// ✅ Allow self-signed for development
type: "HTTPS",
httpConfig: {
allowInsecure: true // Only for development!
}

Health bouncing between healthy/unhealthy?

Fix: Require sustained changes:

// ❌ Too sensitive
consecutiveFails: 1,
consecutiveSuccesses: 1,
// ✅ More stable
consecutiveFails: 3,
consecutiveSuccesses: 3,

Health checks timing out but origin is responding?

Try these:

  1. Increase timeout:
timeout: 10, // Increase from default 5 seconds
  1. Optimize health endpoint:
  • Return cached responses
  • Avoid expensive database queries
  • Use in-memory checks
  • Return early on first successful check
  1. Increase retries:
retries: 5, // Retry more times before marking unhealthy

Getting “expected body not found”?

Debug it:

// Temporarily remove expectedBody to see what's being returned
httpConfig: {
path: "/health",
expectedCodes: ["200"],
// expectedBody: "OK" // Comment out to debug
}

Check your origin’s health endpoint response format:

  • Ensure it returns plain text if using simple string matching
  • Check for leading/trailing whitespace
  • Verify the response isn’t JSON-encoded
  • Use a substring that definitely appears in the response