At 14:17 UTC on October 12, 2024, a single misconfigured Consul 1.20 ACL policy silently broke service discovery for 112 Spring Boot 3.3 microservices, triggering a 47-minute total outage that cost $142k in SLA penalties and engineering time.
π‘ Hacker News Top Stories Right Now
- Where the goblins came from (505 points)
- Noctua releases official 3D CAD models for its cooling fans (174 points)
- The Zig project's rationale for their anti-AI contribution policy (220 points)
- Zed 1.0 (1801 points)
- Craig Venter has died (216 points)
Key Insights
- Consul 1.20's new default
acl.tokens.agent\validation rejected legacy Spring Boot 3.3 Consul client tokens, causing 100% discovery failure for services with >5 minute TTL - Spring Boot 3.3.4 / Spring Cloud Consul 4.1.0 introduced stricter ACL token validation matching Consul 1.20's breaking changes
- Outage cost $142k in SLA penalties, 312 engineering hours, and 0.12% monthly churn for our SaaS platform
- By Q3 2025, 70% of Consul-managed Spring Boot deployments will adopt automated ACL policy validation in CI pipelines, up from 12% today
Incident Timeline
Our team had been running Consul 1.19.4 and Spring Boot 3.2.2 for 14 months without major incidents. On October 10, 2024, we started a planned upgrade to Consul 1.20.0 and Spring Boot 3.3.4 to take advantage of Consul's new ACL audit logging and Spring Boot 3.3's Java 21 support. The upgrade was staged: dev (Oct 10), staging (Oct 11), production (Oct 12 14:00 UTC). Staging passed all 1,200 integration tests, including Consul service registration tests. At 14:17 UTC on Oct 12, the production upgrade completed. Within 3 minutes, our PagerDuty alert for API success rate dropped below 1% triggered. The SRE team initially suspected a network issue, but within 12 minutes, we identified that 112 of 140 microservices were not registered in Consul. At 14:42 UTC, we rolled back Consul to 1.19.4, which restored service at 14:52 UTC. The total outage was 47 minutes. Post-outage analysis found the root cause was a misconfigured Consul 1.20 ACL agent token, which we had not updated during the upgrade.
Root Cause Analysis
Consul 1.20 (https://github.com/hashicorp/consul) introduced a breaking change to the default acl.tokens.agent\ validation: agent tokens now require service:write\ permission to register services, whereas Consul 1.19 only required service:read\. Spring Boot 3.3 (https://github.com/spring-projects/spring-boot) ships with Spring Cloud Consul 4.1.0 (https://github.com/spring-cloud/spring-cloud-consul), which updated the Consul client to use the new ACL API v2, enforcing the stricter token validation. Our Consul 1.20 config (consul.hcl) had the agent token set to a legacy token with only service:read\ permission, which was sufficient for Consul 1.19 but invalid for 1.20. When we upgraded, all services using the agent token failed to register, causing service discovery to return empty lists for all service lookups, leading to 100% API failure. The error was silent in the Consul logs initially, as Consul 1.20 logs ACL denials at INFO level by default, which our logging config was filtering out. We only caught the error when we tailed the Spring Boot app logs, which showed ConsulRegistrationException: ACL token does not have service:write permission\ for every service.
Code Example 1: Spring Boot 3.3 Consul Discovery Configuration
package com.example.inventory.config;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryClient;
import org.springframework.cloud.consul.serviceregistry.ConsulRegistration;
import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.agent.model.Registration;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
/**
* Custom Consul discovery configuration for Spring Boot 3.3.
* Aligns with Consul 1.20+ ACL requirements to avoid silent registration failures.
*/
@Configuration
@EnableAsync
public class ConsulDiscoveryConfig {
private static final Logger log = LoggerFactory.getLogger(ConsulDiscoveryConfig.class);
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final long BASE_BACKOFF_MS = 1000L;
private final ConsulClient consulClient;
private final ConsulServiceRegistry serviceRegistry;
public ConsulDiscoveryConfig(ConsulClient consulClient, ConsulServiceRegistry serviceRegistry) {
this.consulClient = consulClient;
this.serviceRegistry = serviceRegistry;
}
/**
* Registers the current service with Consul 1.20+ with strict ACL validation.
* Retries up to 3 times with exponential backoff on transient failures.
* Logs and alerts on persistent ACL policy errors.
*/
@Bean
@Retryable(maxAttempts = MAX_RETRY_ATTEMPTS, backoff = @Backoff(delay = BASE_BACKOFF_MS, multiplier = 2))
public ConsulRegistration consulRegistration() {
try {
String serviceName = \"inventory-service\";
String serviceId = serviceName + \"-\" + InetAddress.getLocalHost().getHostAddress() + \"-\" + System.currentTimeMillis();
int port = 8080; // Matches server.port in application.yml
Registration registration = new Registration();
registration.setId(serviceId);
registration.setName(serviceName);
registration.setPort(port);
registration.setAddress(InetAddress.getLocalHost().getHostAddress());
// Critical: Add Consul 1.20+ required service tags for ACL validation
Map tags = new HashMap<>();
tags.put(\"version\", \"1.2.4\");
tags.put(\"env\", \"prod\");
tags.put(\"acl-policy\", \"service-write\"); // Matches Consul 1.20 agent token policy
registration.setTags(tags);
// Health check configuration compatible with Consul 1.20 defaults
Registration.Check check = new Registration.Check();
check.setHttp(\"http://\" + registration.getAddress() + \":\" + port + \"/actuator/health/liveness\");
check.setInterval(\"10s\");
check.setTimeout(\"5s\");
check.setDeregisterCriticalServiceAfter(\"30m\");
registration.setCheck(check);
ConsulRegistration consulReg = new ConsulRegistration(registration, consulClient);
serviceRegistry.register(consulReg);
log.info(\"Successfully registered service {} with Consul 1.20+\", serviceId);
return consulReg;
} catch (UnknownHostException e) {
log.error(\"Failed to resolve local host address for Consul registration\", e);
throw new RuntimeException(\"Host resolution failed\", e);
} catch (Exception e) {
// Catch ACL validation errors specific to Consul 1.20+
if (e.getMessage().contains(\"ACL token does not have service:write permission\")) {
log.error(\"CRITICAL: Consul agent token lacks required service:write permission for Consul 1.20+. \" +
\"Update consul.hcl acl.tokens.agent policy immediately.\", e);
// Trigger PagerDuty alert in production
triggerAlert(\"Consul ACL Policy Violation\", e.getMessage());
}
log.error(\"Consul registration failed after {} attempts\", MAX_RETRY_ATTEMPTS, e);
throw e;
}
}
@Async
void triggerAlert(String title, String message) {
// Production alerting logic (PagerDuty, Slack, etc.)
log.warn(\"ALERT: {} - {}\", title, message);
}
}
Code Example 2: Consul 1.20 ACL Policy Validator
#!/usr/bin/env python3
\"\"\"
Consul 1.20 ACL Policy Validator for Spring Boot 3.3 Deployments
Validates that agent tokens have required permissions to avoid service discovery failures.
\"\"\"
import requests
import json
import sys
import argparse
from typing import Dict, List, Tuple
# Consul API defaults
CONSUL_API_BASE = \"http://localhost:8500/v1\"
REQUIRED_POLICY_PERMISSIONS = [\"service:write\", \"node:read\", \"agent:register\"]
class ConsulACLValidator:
def __init__(self, consul_url: str, management_token: str):
self.consul_url = consul_url.rstrip('/')
self.management_token = management_token
self.headers = {\"X-Consul-Token\": management_token}
self.errors: List[str] = []
self.warnings: List[str] = []
def validate_agent_token(self, agent_token: str) -> bool:
\"\"\"
Validates that the agent token has all required permissions for Spring Boot 3.3 clients.
Returns True if valid, False otherwise.
\"\"\"
try:
# Fetch token policy from Consul API
resp = requests.get(
f\"{self.consul_url}/acl/token/{agent_token}\",
headers=self.headers,
timeout=5
)
resp.raise_for_status()
token_data = resp.json()
token_policies = [p[\"name\"] for p in token_data.get(\"Policies\", [])]
# Check for required policy attachments
required_policies = [\"service-write-prod\", \"agent-register-prod\"]
for policy in required_policies:
if policy not in token_policies:
self.errors.append(f\"Agent token missing required policy: {policy}\")
# Validate individual policy permissions
for policy_name in token_policies:
policy_resp = requests.get(
f\"{self.consul_url}/acl/policy/{policy_name}\",
headers=self.headers,
timeout=5
)
if not policy_resp.ok:
self.warnings.append(f\"Failed to fetch policy {policy_name}: {policy_resp.status_code}\")
continue
policy_rules = policy_resp.json().get(\"Rules\", \"\")
for required_perm in REQUIRED_POLICY_PERMISSIONS:
if required_perm not in policy_rules:
self.errors.append(
f\"Policy {policy_name} missing required permission: {required_perm}\"
)
# Test service registration with agent token (dry run)
test_registration = {
\"Name\": \"consul-acl-validation-test\",
\"Port\": 9999,
\"Address\": \"127.0.0.1\",
\"Check\": {\"HTTP\": \"http://localhost:9999/health\", \"Interval\": \"10s\"}
}
reg_resp = requests.put(
f\"{self.consul_url}/agent/service/register\",
headers={\"X-Consul-Token\": agent_token},
json=test_registration,
timeout=5
)
if not reg_resp.ok:
self.errors.append(f\"Service registration dry run failed: {reg_resp.text}\")
else:
# Clean up test service
requests.put(
f\"{self.consul_url}/agent/service/deregister/consul-acl-validation-test\",
headers={\"X-Consul-Token\": agent_token},
timeout=5
)
return len(self.errors) == 0
except requests.exceptions.RequestException as e:
self.errors.append(f\"Consul API request failed: {str(e)}\")
return False
except json.JSONDecodeError as e:
self.errors.append(f\"Failed to parse Consul API response: {str(e)}\")
return False
except Exception as e:
self.errors.append(f\"Unexpected validation error: {str(e)}\")
return False
def print_results(self) -> None:
if self.errors:
print(\"β VALIDATION FAILED\")
for err in self.errors:
print(f\" - {err}\")
else:
print(\"β
VALIDATION PASSED\")
if self.warnings:
print(\"β οΈ WARNINGS\")
for warn in self.warnings:
print(f\" - {warn}\")
if __name__ == \"__main__\":
parser = argparse.ArgumentParser(description=\"Validate Consul 1.20 ACL policies for Spring Boot 3.3\")
parser.add_argument(\"--consul-url\", default=\"http://localhost:8500\", help=\"Consul API URL\")
parser.add_argument(\"--mgmt-token\", required=True, help=\"Consul management token\")
parser.add_argument(\"--agent-token\", required=True, help=\"Agent token to validate\")
args = parser.parse_args()
validator = ConsulACLValidator(args.consul_url, args.mgmt_token)
is_valid = validator.validate_agent_token(args.agent_token)
validator.print_results()
sys.exit(0 if is_valid else 1)
Code Example 3: Spring Boot 3.3 Consul Integration Test
package com.example.inventory.integration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryClient;
import org.springframework.cloud.consul.serviceregistry.ConsulRegistration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.consul.ConsulContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.agent.model.Service;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Integration test for Consul 1.20 service registration with Spring Boot 3.3.
* Uses Testcontainers (https://github.com/testcontainers/testcontainers-java) to spin up a real Consul 1.20 instance for validation.
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class ConsulRegistrationIntegrationTest {
private static final String CONSUL_VERSION = \"hashicorp/consul:1.20.0\";
private static final String AGENT_TOKEN = \"consul-agent-token-prod\";
private static final String MANAGEMENT_TOKEN = \"consul-management-token-prod\";
@Container
static ConsulContainer consulContainer = new ConsulContainer(
DockerImageName.parse(CONSUL_VERSION))
.withExposedPorts(8500)
.withEnv(\"CONSUL_LOCAL_CONFIG\", \"\"\"
{
\"acl\": {
\"enabled\": true,
\"default_policy\": \"deny\",
\"tokens\": {
\"master\": \"%s\",
\"agent\": \"%s\"
}
}
}
\"\"\".formatted(MANAGEMENT_TOKEN, AGENT_TOKEN));
@DynamicPropertySource
static void registerConsulProperties(DynamicPropertyRegistry registry) {
registry.add(\"spring.cloud.consul.host\", consulContainer::getHost);
registry.add(\"spring.cloud.consul.port\", () -> consulContainer.getFirstMappedPort());
registry.add(\"spring.cloud.consul.discovery.prefer-ip-address\", () -> true);
registry.add(\"spring.cloud.consul.token\", () -> AGENT_TOKEN);
registry.add(\"spring.cloud.consul.acl-token\", () -> AGENT_TOKEN);
}
@Autowired
private ConsulDiscoveryClient discoveryClient;
@Autowired
private ConsulClient consulClient;
@BeforeEach
void setup() {
// Create required ACL policies in Consul for testing
createTestPolicies();
}
@Test
void testServiceRegistrationSucceedsWithValidAgentToken() {
// Given: Valid agent token with service:write permission
String serviceName = \"inventory-service\";
// When: Fetch registered services from Consul
Map services = consulClient.getAgentServices(AGENT_TOKEN).getValue();
// Then: Service is registered successfully
Optional registeredService = services.values().stream()
.filter(s -> s.getService().equals(serviceName))
.findFirst();
assertThat(registeredService).isPresent();
assertThat(registeredService.get().getPort()).isPositive();
assertThat(registeredService.get().getTags()).contains(\"acl-policy=service-write\");
}
@Test
void testServiceRegistrationFailsWithInvalidAgentToken() {
// Given: Agent token with only service:read permission
String invalidToken = \"invalid-agent-token\";
// Update Spring context to use invalid token (simplified for test)
// In real test, would reinitialize ConsulClient with invalid token
// When/Then: Registration fails with ACL permission error
assertThatThrownBy(() -> {
consulClient.agentServiceRegister(
new com.ecwid.consul.v1.agent.model.Registration(),
invalidToken
);
}).hasMessageContaining(\"ACL token does not have permission\");
}
private void createTestPolicies() {
// Create service-write policy
String serviceWritePolicy = \"\"\"
{
\"Name\": \"service-write-prod\",
\"Rules\": \"service \\\"inventory-service\\\" { policy = \\\"write\\\" }\"
}
\"\"\";
consulClient.setPolicy(serviceWritePolicy, MANAGEMENT_TOKEN);
// Create agent-register policy
String agentRegisterPolicy = \"\"\"
{
\"Name\": \"agent-register-prod\",
\"Rules\": \"agent \\\"\\\" { policy = \\\"register\\\" }\"
}
\"\"\";
consulClient.setPolicy(agentRegisterPolicy, MANAGEMENT_TOKEN);
// Attach policies to agent token
String attachPolicyPayload = \"\"\"
{
\"Token\": \"%s\",
\"Policies\": [{\"Name\": \"service-write-prod\"}, {\"Name\": \"agent-register-prod\"}]
}
\"\"\".formatted(AGENT_TOKEN);
consulClient.aclTokenUpdate(attachPolicyPayload, MANAGEMENT_TOKEN);
}
}
Consul 1.19 vs 1.20: Breaking Changes Impacting Spring Boot 3.3
Consul 1.19 vs 1.20: Breaking Changes Impacting Spring Boot 3.3
Feature
Consul 1.19
Consul 1.20
Spring Boot 3.3 Compatibility
Default ACL Agent Token Policy
service:read
service:write
β Breaks <3.3.4 clients
Service Registration Retry Limit
5 attempts
3 attempts
β οΈ Increases failure rate by 40%
Health Check Deregistration Timeout
1h default
30m default
β οΈ Requires config update
ACL Token Validation Latency (p99)
120ms
340ms
β οΈ Adds 220ms to registration
Failed Registration Alerting
Opt-in
Default on
β Improves MTTR by 55%
Spring Cloud Consul 4.1.0 Support
β Full
β Full (with updated token)
N/A
Case Study: Fintech SaaS Platform Outage
- Team size: 6 backend engineers, 2 SREs, 1 engineering manager
- Stack & Versions: Spring Boot 3.3.4, Spring Cloud Consul 4.1.0, Consul 1.20.0, Java 21, Kubernetes 1.29, Prometheus, Grafana, PagerDuty
- Problem: Pre-upgrade p99 service discovery latency was 180ms, but after upgrading Consul to 1.20 and Spring Boot to 3.3, 112 of 140 microservices failed to register, causing 100% API failure for 47 minutes, with p99 latency spiking to 12.8s, SLA breach penalty of $142k, and 312 engineering hours spent on incident response
- Solution & Implementation: 1. Updated Consul 1.20's consul.hcl to set
acl.tokens.agent\to a token withservice:write\andagent:register\policies, 2. Added the Consul ACL validation Python script to CI pipeline to block deployments with invalid tokens, 3. Updated Spring Boot 3.3 config to include required service tags for ACL validation, 4. Added retry logic with exponential backoff to Consul registration, 5. Set up Grafana alerts for Consul ACL failure rate >1% - Outcome: Service registration success rate increased to 99.99%, p99 discovery latency dropped to 210ms, MTTR for Consul-related incidents reduced from 47 minutes to 8 minutes, saving $18k/month in SLA penalties and engineering time
Developer Tips for Consul + Spring Boot 3.3
Tip 1: Always Validate Consul ACL Policies in CI Pipelines
The root cause of our outage was a missing CI check for Consul ACL policies. Consul 1.20's stricter validation means that legacy tokens will fail silently if you don't validate them before deployment. We now run the Python ACL validator script (Code Example 2) as part of every CI pipeline run, blocking deployments if the agent token doesn't have the required permissions. This adds 12 seconds to our CI runtime but has prevented 3 potential outages in the 2 months since implementation. For teams using GitHub Actions, you can add a step like the snippet below. Make sure to store your Consul management and agent tokens as GitHub Secrets, never hardcode them. Additionally, we recommend running the validator against all environments (dev, staging, prod) not just prod, as config drift often starts in lower environments. We also added a weekly scheduled CI run to validate tokens even when no deployments are happening, to catch manual config changes made by SREs. This tip alone reduced our Consul-related incident rate by 82%, and we estimate it saves us ~$9k per month in avoided downtime.
# GitHub Actions step for Consul ACL validation
- name: Validate Consul ACL Policies
run: |
pip install requests
python consul_acl_validator.py \
--consul-url https://consul.prod.example.com \
--mgmt-token ${{ secrets.CONSUL_MGMT_TOKEN }} \
--agent-token ${{ secrets.CONSUL_AGENT_TOKEN }}
if: always()
Tip 2: Add Retry Logic with Exponential Backoff to Consul Registration
Spring Boot 3.3's default Consul client does not have retry logic for registration failures, which means transient Consul errors (like network blips or temporary ACL validation latency) will cause permanent registration failures. Our custom ConsulDiscoveryConfig (Code Example 1) adds Spring Retry with 3 attempts and exponential backoff, which reduced our registration failure rate from 12% to 0.3% in testing. Consul 1.20's ACL validation latency is 340ms p99, which is 2.8x higher than Consul 1.19, so retries are even more critical now. We also added async alerting for persistent failures, so our SRE team gets paged within 1 minute of a registration failure, rather than waiting for customer reports. For teams not using Spring Retry, you can implement retry logic manually with a loop and Thread.sleep, but Spring Retry is more maintainable and has built-in metrics for retry attempts. We also recommend setting a maximum retry interval of 30 seconds to avoid blocking application startup for too long. This tip improved our application startup success rate from 94% to 99.8%, even during Consul maintenance windows.
// Manual retry logic for Consul registration (if not using Spring Retry)
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
consulClient.agentServiceRegister(registration);
break;
} catch (Exception e) {
retryCount++;
Thread.sleep(1000 * (long) Math.pow(2, retryCount));
}
}
Tip 3: Use Testcontainers for Local Consul Integration Testing
Most teams test Consul integration by mocking the Consul client, which misses breaking changes like Consul 1.20's ACL updates. Our integration test (Code Example 3) uses Testcontainers (https://github.com/testcontainers/testcontainers-java) to spin up a real Consul 1.20 instance, so we catch registration failures locally before pushing to CI. This reduced our CI failure rate from 18% to 2% for Consul-related changes. Testcontainers Consul module supports all Consul versions, so you can test against specific versions (like 1.20.0) to match your production environment. We also added a nightly test run against the latest Consul RC to catch breaking changes early, which alerted us to Consul 1.20's ACL changes 3 weeks before GA. For local development, we recommend using the Testcontainers desktop app to manage Consul containers, so you don't have to run Consul locally. We also added assertions for service tags and health checks in our integration tests, to catch misconfigurations that don't cause hard failures but lead to discovery issues. This tip has saved our team ~40 engineering hours per month in debugging CI failures, and we estimate it prevents 1 outage per quarter.
// Testcontainers Consul container setup for local testing
ConsulContainer consul = new ConsulContainer(\"hashicorp/consul:1.20.0\")
.withEnv(\"CONSUL_LOCAL_CONFIG\", \"{ \\\"acl\\\": { \\\"enabled\\\": true } }\");
consul.start();
// Use consul.getHost() and consul.getFirstMappedPort() to configure Spring Boot
Join the Discussion
We've shared our postmortem and mitigation steps, but we want to hear from other teams running Consul and Spring Boot in production. Share your experiences with Consul 1.20 upgrades, ACL policy management, or Spring Boot 3.3 migration pain points.
Discussion Questions
- What percentage of your Consul-managed services have adopted automated ACL policy validation in CI pipelines, and what tools do you use?
- When upgrading to Consul 1.20, would you prioritize backward compatibility with legacy clients or enforce stricter ACL defaults, and why?
- How does Consul 1.20's ACL validation latency compare to alternative service discovery tools like etcd 3.5 or Eureka 2.0 for your workload?
Frequently Asked Questions
Q: Can I run Consul 1.20 with Spring Boot 3.2 or earlier without upgrading?
A: No, Spring Boot 3.2 and earlier use Spring Cloud Consul 3.x, which does not support Consul 1.20's new ACL API. You will encounter the same registration failures we saw. You must either upgrade to Spring Boot 3.3+ (Spring Cloud Consul 4.1.0+) or stay on Consul 1.19 until you can upgrade your Spring Boot version. We recommend testing the upgrade in staging for 2 weeks before rolling out to production, as there are other breaking changes in Spring Boot 3.3 (like Jakarta EE 10 migration) that may impact your app.
Q: How do I migrate legacy Consul ACL tokens to Consul 1.20's required format?
A: Use the Consul 1.20 CLI command consul acl token update\ to add the service:write\ policy to your existing agent tokens. First, create a new policy with the required permissions, then attach it to the agent token. We recommend rotating agent tokens after migration to avoid using legacy tokens long-term. Our Python validator script can verify that the migrated token has the correct permissions. Never delete the old token until you've verified that all services are registering successfully with the new token.
Q: What metrics should I monitor to catch Consul 1.20 config errors early?
A: Monitor the following metrics: 1. consul.agent.service.registration.failure.count\ (alert on >5 in 5 minutes), 2. consul.acl.validation.latency.p99\ (alert on >500ms), 3. spring.cloud.consul.registration.success.rate\ (alert on <99.9%), 4. \`consul.agent.token.permission.denied.count\` (alert on >0). We use Prometheus to scrape these metrics and Grafana to visualize them, with PagerDuty alerts for all critical thresholds. We also recommend logging all Consul client errors at WARN level or higher, to catch issues that don't trigger metric thresholds.
Conclusion & Call to Action
Our outage was entirely preventable with basic CI checks and retry logic. Consul 1.20's breaking changes are well-documented, but the Spring Boot 3.3 client updates mean you can't upgrade one without the other. Our opinionated recommendation: always validate Consul ACL policies in CI, add retry logic to service registration, and use Testcontainers for integration testing. Never upgrade Consul or Spring Boot in production without testing the full stack in staging first. The cost of prevention is a fraction of the cost of an outage: our $142k outage could have been prevented with a $0 open-source script and 12 seconds of CI time.
$142kTotal cost of our 47-minute Consul 1.20 config error outage











