Home Java Spring Cloud Bus: Broadcasting Config Changes & Events Across Microservices
Advanced 6 min · July 14, 2026

Spring Cloud Bus: Broadcasting Config Changes & Events Across Microservices

Learn how to use Spring Cloud Bus to broadcast config changes and events across microservices.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+
  • Spring Boot 3.x
  • RabbitMQ or Kafka running locally (or in Docker)
  • Basic understanding of Spring Cloud Config
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Spring Cloud Bus links microservices via a lightweight message broker (RabbitMQ or Kafka).
  • It broadcasts RefreshRemoteApplicationEvent to all instances when config changes.
  • Enables dynamic config refresh without restarting each service individually.
  • Works with Spring Cloud Config Server to push changes instantly.
  • Avoids polling overhead and manual refresh calls.
✦ Definition~90s read
What is Spring Cloud Bus?

Spring Cloud Bus is a distributed event bus that links microservices via a message broker, enabling broadcast of configuration changes and custom events across all instances.

Imagine you're the manager of a large restaurant chain.
Plain-English First

Imagine you're the manager of a large restaurant chain. Instead of calling each branch individually to update the menu, you send one message to a group chat. Spring Cloud Bus is that group chat for your microservices — one message updates everyone at once.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

If you've ever managed a fleet of microservices, you know the pain of updating configuration. You change a property in your Config Server, then SSH into each box and hit /actuator/refresh — or worse, restart the whole service. That's 2005 thinking. Spring Cloud Bus solves this by turning your services into a mesh that listens to a message broker. When you trigger a refresh, it broadcasts an event to every connected instance. No polling, no manual curl commands. Just one endpoint call, and your entire system picks up the new values.

I've seen teams with 50+ microservices waste entire deployment windows just to change a database connection timeout. With Bus, that same change takes seconds. But here's the kicker: it's not just for config. You can broadcast any custom event — cache invalidation, feature flags, schema migrations — across your entire ecosystem. The pattern is simple: publish to a broker topic, consume in every service that cares.

In this guide, I'll walk you through setting up Spring Cloud Bus with RabbitMQ (the most common choice), show you how to broadcast config changes and custom events, and share the gotchas that only appear under production load. By the end, you'll know when to use Bus and when to reach for something else.

What is Spring Cloud Bus?

Spring Cloud Bus links multiple microservice instances over a lightweight message broker. It's essentially a distributed event bus. The most common use case is broadcasting configuration changes from Spring Cloud Config Server to all connected services. But you can also send arbitrary events — think cache invalidation, feature flag toggles, or even schema migration triggers.

Under the hood, Bus uses either RabbitMQ or Kafka. RabbitMQ is the default and more common in Spring Boot apps. Each service instance declares a unique queue bound to a topic exchange. When you hit /actuator/bus-refresh, the Config Server publishes a RefreshRemoteApplicationEvent to the broker. Every instance picks it up and refreshes its @RefreshScope beans.

Here's the key: Bus is not for high-frequency event streaming. It's for control-plane operations — changes that happen infrequently but must reach every instance. Don't use it for real-time data pipelines. Use Kafka Streams or similar for that.

`` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> ``

``yaml spring: rabbitmq: host: localhost port: 5672 username: guest password: guest cloud: bus: enabled: true id: ${spring.application.name}:${spring.application.instance_id:${random.value}} ``

The bus.id must be unique per instance. The default pattern uses application:port but you can customize.

``yaml management: endpoints: web: exposure: include: bus-refresh ``

That's it. Start your services, then POST to /actuator/bus-refresh on any instance. All instances will refresh.

BusConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class BusEventListener {

    @EventListener
    public void handleRefresh(RefreshRemoteApplicationEvent event) {
        System.out.println("Received refresh event from: " + event.getOriginService());
    }
}
Output
Received refresh event from: config-server:8080
💡Bus ID Uniqueness
📊 Production Insight
I've seen teams forget to set unique bus IDs when deploying multiple replicas. The result: only one replica receives the refresh, leaving others stale. Always include ${random.value} in your bus id.
🎯 Key Takeaway
Spring Cloud Bus turns your microservices into a distributed event mesh using RabbitMQ or Kafka, primarily for config propagation.

Broadcasting Config Changes with Bus

The killer feature of Spring Cloud Bus is dynamic config refresh. You change a property in your Config Server repository, then trigger a refresh via Bus. All services pick up the change without restart.

Here's the flow: 1. Config Server hosts properties (e.g., in Git). 2. You update a property and push to Git. 3. You POST to /actuator/bus-refresh on any service (or Config Server). 4. Config Server publishes RefreshRemoteApplicationEvent. 5. Each service instance receives the event and refreshes its @RefreshScope beans.

But there's a catch: not all beans should be @RefreshScope. Only beans that hold configuration properties need it. Overusing @RefreshScope can cause memory leaks because the old bean context is not garbage collected until all references are released.

```java @RefreshScope @Component public class DatabaseConfig {

@Value("${database.max-connections:10}") private int maxConnections;

public int getMaxConnections() { return maxConnections; } } ```

When refresh happens, this bean is recreated with the new value. But if you have a long-lived reference to it (e.g., stored in a static field), the old bean never gets garbage collected. That's a memory leak.

Another gotcha: @ConfigurationProperties classes are automatically refreshable if they are used in @RefreshScope beans. But if you inject them directly without @RefreshScope, they won't refresh.

Best practice: Use @ConfigurationProperties with a @RefreshScope wrapper, or use the Environment directly.

``java @RefreshScope @ConfigurationProperties(prefix = "database") @Component public class DatabaseProperties { private int maxConnections; // getters and setters } ``

Now, when Bus triggers refresh, DatabaseProperties gets new values.

RefreshController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope
public class ConfigController {

    @Autowired
    private DatabaseProperties dbProps;

    @GetMapping("/config")
    public String getConfig() {
        return "Max connections: " + dbProps.getMaxConnections();
    }
}
Output
Max connections: 20
⚠ RefreshScope and Memory Leaks
📊 Production Insight
At a fintech startup, we had a @RefreshScope bean injected into a singleton scheduler. After a few refreshes, memory usage spiked. We found dozens of stale beans in heap dumps. Solution: use @Scope(value = "refresh", proxyMode = ScopedProxyMode.TARGET_CLASS) and inject via ObjectFactory.
🎯 Key Takeaway
Use @RefreshScope judiciously. Only beans holding configuration properties need it. Singleton references to scoped beans cause memory leaks.

Broadcasting Custom Events with Bus

Config refresh is just one use case. You can broadcast any custom event across all services. For example, invalidate a cache in all instances, toggle a feature flag, or trigger a background job.

```java public class CacheInvalidationEvent extends RemoteApplicationEvent { private String cacheName; private String key;

// constructor, getters, setters public CacheInvalidationEvent() { // for serialization }

public CacheInvalidationEvent(Object source, String originService, String destinationService, String cacheName, String key) { super(source, originService, destinationService); this.cacheName = cacheName; this.key = key; } } ```

```java @Service public class CacheService {

@Autowired private ApplicationEventPublisher publisher;

public void invalidateAll(String cacheName) { CacheInvalidationEvent event = new CacheInvalidationEvent( this, "my-service", "**", cacheName, null ); publisher.publishEvent(event); } } ```

The destination * means all services. You can target specific services with a pattern like my-service:.

```java @Component public class CacheInvalidationListener {

@EventListener public void handleInvalidation(CacheInvalidationEvent event) { String cacheName = event.getCacheName(); String key = event.getKey(); // evict from local cache System.out.println("Invalidating cache: " + cacheName + " key: " + key); } } ```

One thing the docs gloss over: serialization. RemoteApplicationEvent is serialized and deserialized via JSON (Jackson). Your custom event must have a no-arg constructor and getters/setters. Also, the class must be on the classpath of all services. Otherwise, deserialization fails silently.

Another gotcha: the originService and destinationService fields are used for routing. If you set destinationService to **, it goes to all. But if you set it to a specific service ID, only that service receives it. The service ID is the spring.cloud.bus.id value.

CustomEventPublisher.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class CustomEventPublisher {

    @Autowired
    private ApplicationEventPublisher publisher;

    public void publishEvent(String message) {
        RemoteApplicationEvent event = new RemoteApplicationEvent(
            this, "my-service", "**"
        ) {
            // anonymous subclass with custom payload
            private String payload = message;
            public String getPayload() { return payload; }
            public void setPayload(String payload) { this.payload = payload; }
        };
        publisher.publishEvent(event);
    }
}
🔥Event Serialization Gotcha
📊 Production Insight
We once had a custom event that caused a serialization error because the class was in a shared library but not on the classpath of one service. Bus silently dropped the event. We only noticed when cache invalidation stopped working. Solution: add a @JsonTypeInfo annotation to the event for polymorphic deserialization.
🎯 Key Takeaway
Extend RemoteApplicationEvent for custom events. Ensure serialization compatibility across services. Use destination patterns to target specific services.

What the Official Docs Won't Tell You

I've been using Spring Cloud Bus since the Dalston release, and the official docs hide some sharp edges. Here are the ones that bit me hardest.

1. Bus is NOT reliable for critical events. Bus uses RabbitMQ's default exchange with transient queues. If a service disconnects and reconnects, it may miss events published during the downtime. There's no guaranteed delivery. For critical config changes, implement a fallback: poll Config Server periodically as a safety net.

2. The /bus-refresh endpoint is not idempotent by default. If you call it twice in quick succession, every instance refreshes twice. That can cause duplicate side effects. Use a distributed lock or deduplication on the event ID.

3. Memory leaks with @RefreshScope are real. I've seen production OutOfMemoryErrors because a @RefreshScope bean was injected into a singleton. The singleton holds a reference to the old bean, preventing GC. Use @Scope(value = "refresh", proxyMode = ScopedProxyMode.TARGET_CLASS) and inject via ObjectFactory if you need to inject into singletons.

4. Bus events are serialized as JSON using Jackson. But the default ObjectMapper may not handle your custom types. If you use Lombok's @Data, Jackson works fine. But if you have complex nested objects, you might need to configure Jackson serialization.

5. The destinationService field is case-sensitive. I spent hours debugging why events weren't reaching a service named payment-service when I sent to Payment-Service. Bus matches exactly.

6. Bus doesn't work well with multiple Config Servers. If you have multiple Config Server instances behind a load balancer, Bus events may not propagate correctly because each Config Server has its own broker connection. Use a single Config Server or a shared broker.

7. The spring.cloud.bus.id must be unique per instance. If you deploy multiple replicas with the same ID, only one will receive events. Always include ${random.value} or a unique instance ID.

8. Bus adds latency to every startup. Each instance declares a queue and binds to the exchange. If you have 100 instances, that's 100 queue declarations. RabbitMQ can handle it, but it adds startup time. Consider using a lazy queue or a separate vhost for Bus.

SafeRefreshBean.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "refresh", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SafeRefreshBean {

    @Autowired
    private ObjectFactory<DatabaseProperties> dbPropsFactory;

    public int getMaxConnections() {
        return dbPropsFactory.getObject().getMaxConnections();
    }
}
⚠ Bus is not for critical events without fallback
📊 Production Insight
At a SaaS company, we used Bus for feature flag toggles. A network partition caused some services to miss the toggle event. Users saw inconsistent behavior. We added a polling fallback from Config Server every 30 seconds. Problem solved.
🎯 Key Takeaway
The official docs miss several production gotchas: transient queues, memory leaks, case-sensitive destinations, and serialization issues. Always test with a distributed setup.

Bus vs. Manual Refresh: When to Use Which

You might be tempted to use Bus everywhere. Don't. Here's my rule of thumb: use Bus when you need to propagate a change to every instance within seconds, and you can tolerate the occasional missed event. Use manual refresh (calling /actuator/refresh on each instance) when you need guaranteed delivery or when the change is critical.

When to use Bus: - Changing log levels across all services. - Toggling feature flags (non-critical). - Invalidating caches. - Updating non-critical configuration like rate limits (with fallback).

When to avoid Bus: - Changing database connection strings (use manual refresh with blue-green deployment). - Updating authentication secrets (use restart). - Critical security patches (restart is safer).

Another pattern: use Bus for broadcast, but have each service also poll Config Server every few minutes. That way, if Bus misses an event, the poll catches it.

PollingConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.cloud.context.refresh.ContextRefresher;

@Component
public class PollingConfigRefresher {

    private final ContextRefresher contextRefresher;

    public PollingConfigRefresher(ContextRefresher contextRefresher) {
        this.contextRefresher = contextRefresher;
    }

    @Scheduled(fixedDelay = 300000) // 5 minutes
    public void refresh() {
        contextRefresher.refresh();
    }
}
💡Hybrid Approach
📊 Production Insight
We lost a customer because a Bus event missed a rate limit change, causing an API spike that brought down the service. Now we always have a polling fallback. The 5-minute delay is acceptable for rate limits.
🎯 Key Takeaway
Bus is great for non-critical, fast propagation. For critical config, use manual refresh or a hybrid approach with polling.

Testing Spring Cloud Bus in a Distributed Environment

Testing Bus locally is easy — you can run RabbitMQ in Docker and a few service instances. But testing in a CI pipeline with multiple services is tricky. Here's how we do it.

Unit Testing: You can test the event publishing and listening logic without a broker. Use @SpringBootTest with spring.cloud.bus.enabled=false and mock the ApplicationEventPublisher.

Integration Testing: Use Testcontainers to spin up RabbitMQ in your tests. Then start multiple service instances and verify that events propagate.

```java @SpringBootTest(properties = { "spring.cloud.bus.enabled=true", "spring.rabbitmq.host=localhost", "spring.rabbitmq.port=5672" }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class BusIntegrationTest {

@Autowired private ApplicationEventPublisher publisher;

@Test @Order(1) void testCustomEventBroadcast() { // publish event publisher.publishEvent(new CacheInvalidationEvent(this, "test-service", "**", "users", "123")); // give time for propagation Thread.sleep(1000); // assert that listener in another service received it (via shared state) } } ```

But testing across multiple JVMs requires a more elaborate setup. We use a separate test suite that starts multiple Spring Boot applications with different ports and uses a shared RabbitMQ instance.

Gotcha: In tests, Bus events are processed asynchronously. You need to wait for them. Use Awaitility to poll for expected state.

Another tip: disable Bus in unit tests to avoid needing RabbitMQ. Use @SpringBootTest(properties = "spring.cloud.bus.enabled=false").

BusTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;

@SpringBootTest(properties = "spring.cloud.bus.enabled=false")
class BusUnitTest {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Test
    void testPublishWithoutBroker() {
        // This test does not require RabbitMQ
        publisher.publishEvent(new RefreshRemoteApplicationEvent(this, "test", "**"));
        // Assert that listener was called (if mocked)
    }
}
🔥Testcontainers for Integration Tests
📊 Production Insight
We once had a bug that only appeared when Bus was enabled and the broker had a specific configuration. Unit tests didn't catch it. Integration tests with Testcontainers did. Always include a full integration test in your pipeline.
🎯 Key Takeaway
Test Bus logic with mocks for unit tests, and use Testcontainers for integration tests that verify event propagation across services.
● Production incidentPOST-MORTEMseverity: high

The Midnight Config Cascade That Took Down Payments

Symptom
All payment-processing services started throwing connection pool exhaustion errors at 2:13 AM. Users saw 'Payment gateway timeout'.
Assumption
The ops team assumed it was a network issue or DDoS attack.
Root cause
A developer updated a max-connections property in Config Server. Spring Cloud Bus broadcast the change to 12 instances at once. Each instance recreated its connection pool, causing a thundering herd on the database. The old pool was closed before the new one was ready, leading to connection failures.
Fix
Added a grace period in the @RefreshScope bean to drain old connections before creating new ones. Also implemented a staggered refresh using a custom event with a random delay.
Key lesson
  • Always test config changes with a canary instance first.
  • Use a distributed rate limiter on external resources during refresh.
  • Implement connection pool warmup after refresh.
  • Consider using a 'draining' phase in your beans to avoid abrupt cuts.
  • Monitor the refresh event flow — Bus can amplify mistakes instantly.
Production debug guideSymptom to Action4 entries
Symptom · 01
Config changes not propagating to some instances
Fix
Check if the instance is connected to the broker. Verify spring.cloud.bus.enabled=true. Look for 'Bus auto-configuration' in logs. Ensure the service has spring-cloud-starter-bus-amqp on classpath.
Symptom · 02
Bus events causing cascading failures (thundering herd)
Fix
Add a random delay before processing the refresh event. Use a custom RemoteApplicationEvent subclass with a delay field.
Symptom · 03
Event not received after broker restart
Fix
Bus uses temporary queues. After broker restart, instances re-subscribe. But if the event was published before reconnection, it's lost. Use a durable subscription or retry logic.
Symptom · 04
Duplicate events received
Fix
Ensure your listener is idempotent. Bus uses at-least-once delivery. Deduplicate using event ID or a distributed lock.
★ Quick Debug Cheat SheetCommon issues and immediate actions for Spring Cloud Bus
Config not refreshing
Immediate action
Check `/actuator/bus-refresh` endpoint accessibility. Verify `management.endpoints.web.exposure.include=bus-refresh`.
Commands
curl -X POST http://localhost:8080/actuator/bus-refresh
Check logs for 'Received remote refresh request'
Fix now
Add spring.cloud.bus.enabled=true and restart.
Event not reaching other services+
Immediate action
Verify broker connectivity. Test with a simple custom event.
Commands
curl -X POST -H "Content-Type: application/json" -d '{"type":"customEvent"}' http://localhost:8080/bus/publish
Check broker management UI for queue activity.
Fix now
Ensure all services share the same spring.cloud.bus.id namespace.
High memory usage after refresh+
Immediate action
Check for `@RefreshScope` beans not being destroyed properly.
Commands
jmap -histo <pid> | grep 'RefreshScope'
Enable debug logging for `o.s.cloud.bus`
Fix now
Add @RefreshScope only to beans that need it. Avoid holding references to scoped beans.
FeatureSpring Cloud BusManual Refresh (/actuator/refresh)
Propagation speedSeconds (near real-time)Variable (depends on manual trigger)
Delivery guaranteeAt-most-once (transient queues)Guaranteed (if you hit every instance)
Ease of useOne endpoint call for allMust call each instance individually
Risk of cascading failureHigh (thundering herd)Low (controlled)
Best forNon-critical config, feature flagsCritical config, security settings
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
BusConfig.java@ComponentWhat is Spring Cloud Bus?
RefreshController.java@RestControllerBroadcasting Config Changes with Bus
CustomEventPublisher.java@ServiceBroadcasting Custom Events with Bus
SafeRefreshBean.java@ComponentWhat the Official Docs Won't Tell You
PollingConfig.java@ComponentBus vs. Manual Refresh
BusTest.java@SpringBootTest(properties = "spring.cloud.bus.enabled=false")Testing Spring Cloud Bus in a Distributed Environment

Key takeaways

1
Spring Cloud Bus broadcasts config changes and custom events across microservices using RabbitMQ or Kafka.
2
Use @RefreshScope sparingly to avoid memory leaks; prefer ObjectFactory for injecting scoped beans into singletons.
3
Always set unique bus IDs per instance to ensure all instances receive events.
4
Bus is not reliable for critical events; implement a polling fallback for guaranteed delivery.
5
Test Bus logic with mocks for unit tests and Testcontainers for integration tests.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Spring Cloud Bus propagate configuration changes?
Q02SENIOR
What are the potential issues with using @RefreshScope in a distributed ...
Q03SENIOR
How can you ensure reliable delivery of config changes when using Spring...
Q01 of 03SENIOR

How does Spring Cloud Bus propagate configuration changes?

ANSWER
Spring Cloud Bus uses a message broker (RabbitMQ or Kafka) to broadcast events. When you POST to /actuator/bus-refresh, the Config Server publishes a RefreshRemoteApplicationEvent. Each service instance listens on a unique queue bound to a topic exchange. Upon receiving the event, it refreshes all @RefreshScope beans, picking up new configuration values from the Config Server.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between Spring Cloud Bus and Spring Cloud Stream?
02
Can I use Spring Cloud Bus without Spring Cloud Config?
03
How do I secure the /actuator/bus-refresh endpoint?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Cloud. Mark it forged?

6 min read · try the examples if you haven't

Previous
Introduction to Spring Cloud Contract: Consumer-Driven Contract Testing
13 / 34 · Spring Cloud
Next
Spring Cloud Consul: Service Discovery and Configuration with HashiCorp Consul