@RefreshScope Misuse in Spring Cloud: Why Java Apps Start Slowly, Time Out, and Trigger Full GC After Scaling

This production troubleshooting write-up for Spring Cloud + Nacos shows that applying @RefreshScope directly to a Controller or Service can trigger lazy-initialization blocking and Bean reconstruction during scaling, restarts, and configuration releases. The result is API timeouts, Full GC, and startup jitter. The core value of this article is to identify the root cause and provide safer alternatives. Keywords: @RefreshScope, Nacos, Full GC

Technical Specifications Snapshot

Parameter Description
Tech stack Java, Spring Boot, Spring Cloud, Nacos
Core issues Slow startup, API timeouts, frequent Full GC, configuration release jitter
Key mechanisms @RefreshScope lazy initialization, refreshAll(), scope-level read/write locks
Service registration protocol Based on the Spring Cloud service registration abstraction, commonly implemented through HTTP and registry communication
Article type Source code analysis + production incident review
Star count Not provided in the original article
Core dependencies spring-cloud-context, spring-cloud-commons, nacos-client

This issue is not caused by a traffic spike but by a misused Bean lifecycle design

When new Pods are added during production scaling, many teams first suspect uneven traffic distribution, conservative JVM settings, or instability in downstream dependencies. But these cases share one pattern: the problem appears only during the initial startup window of new instances and then recovers naturally after a few minutes.

That tells us the bottleneck is not sustained resource exhaustion. Instead, there is a short window during startup in which the instance is already receiving traffic before it is truly ready. If many business Beans in the application are annotated with @RefreshScope, that window becomes significantly larger.

The symptoms map directly to the root causes

Symptom Direct cause
API latency spikes after scaling Requests arrive while the @RefreshScope Bean is still being lazily initialized
Frequent Full GC on new instances Blocked requests pile up, and temporary objects and request context cannot be released in time
API jitter after configuration release refreshAll() destroys cached instances, and the next request must rebuild them under lock

@RefreshScope initializes later than the point at which you assume the service is ready

A normal singleton Bean is instantiated during container startup. Then the web container initializes, and the framework publishes service-ready events. As a result, by the time the instance registers with Nacos, most dependencies are usually already prepared.

But @RefreshScope is not a normal singleton. It uses a proxy and Scope-managed instance lifecycle, and the real object is often created on demand after ContextRefreshedEvent. In essence, it behaves more like a lazily constructed, replaceable object.

Spring Cloud service registration and Web container initialization sequence AI Visual Insight: This diagram shows the order between Spring Cloud service registration and Web container initialization. The key point is that service registration depends on container completion events, which means the registry may begin routing traffic before some @RefreshScope business Beans have finished creating their real instances.

NacosAutoServiceRegistration registration flow AI Visual Insight: This image illustrates how the Nacos client connects to the Spring Cloud registration abstraction through the automatic registration class. The technical takeaway is that exposing a service instance externally is not the same moment as fully constructing the application’s internal Beans.

Pseudocode helps explain the lazy-initialization risk window

@RestController
@RefreshScope // Lazily creates the real object; the first access may trigger initialization
public class OrderController {

    @GetMapping("/ping")
    public String ping() {
        return "ok"; // The first request may pay the object creation cost before business logic runs
    }
}

This example shows that when @RefreshScope is applied to a Controller, the first incoming request may absorb object creation overhead instead of only paying for business execution.

Scope-level read/write locks can amplify concurrent requests into a queueing surge

A more subtle problem lies in the concurrency control inside RefreshScope. Its get() and destroy() operations rely on read/write locks to protect the internal cache. The lock granularity is not tied to a single API endpoint. It covers the entire scope-level object access process.

Once high-concurrency traffic hits a new instance at the same time, multiple threads may end up waiting around the same Bean retrieval, creation, and destruction workflow. The result is not just one slow request. It is a batch of slow requests.

RefreshScope Bean initialization flow AI Visual Insight: This image shows how an @RefreshScope Bean participates in initialization through a ContextRefreshedEvent listener. The important point is that it does not complete creation during standard singleton pre-instantiation, but is instead triggered later through post-startup events and proxy invocation paths.

ContextRefreshedEvent occurs later than WebServerInitializedEvent AI Visual Insight: This diagram highlights the ordering of two key events: the Web server is already initialized and the service may already be registered, while the real construction of the @RefreshScope Bean happens later. That creates a timing overlap between request arrival and Bean creation.

public Object get(String name, ObjectFactory<?> objectFactory) {
    // First read the object from the Scope cache; if missing, enter the controlled creation path
    return cache.computeIfAbsent(name, key -> objectFactory.getObject());
}

public void refreshAll() {
    // Invalidate all cached instances after a configuration change instead of updating a single field
    destroyAllScopedBeans();
}

This pseudocode captures the key fact: the core behavior of @RefreshScope is not “update a property.” It is “discard the instance, then rebuild the instance.”

Configuration hot updates trigger business Bean reconstruction rather than a lightweight refresh

Many teams add @RefreshScope to Controllers and Services so that Nacos configuration changes can take effect automatically. But the source code behavior makes it clear that this is not field-level hot update. It is runtime Bean replacement.

When Nacos detects a configuration change, Spring Cloud publishes a refresh event. Then RefreshScope.refreshAll() invalidates the related instances. Note that the framework usually does not rebuild them immediately. It waits until the next invocation to create a new object.

Path from Nacos configuration change to RefreshEvent AI Visual Insight: This image shows the full path in which the Nacos client pre-registers listeners, receives configuration changes, and publishes a Spring refresh event. The key technical point is that a configuration center event is ultimately transformed into a refresh broadcast inside the Spring container.

refreshAll triggers Bean invalidation and reconstruction AI Visual Insight: This diagram emphasizes that refreshAll() does not perform a localized property write. It bulk-destroys the cached instances marked with @RefreshScope, which means any configuration release can push subsequent requests onto the re-instantiation path.

This is why runtime configuration release can also trigger P1 alerts

After a configuration release, if the first batch of requests happens to hit a Bean that was just invalidated, those requests may repeatedly pay for locking, construction, dependency injection, and proxy switching. For Services with deep dependency chains or heavy initialization logic, that cost is enough to expand into second-level timeouts.

The correct solution is to restrict dynamic configuration to configuration objects instead of business objects

A safer approach is to centralize mutable configuration inside @ConfigurationProperties classes, while keeping business Beans as stable singletons that only consume the latest configuration values. This way, the framework updates properties instead of replacing Controller or Service instances.

@Component
@ConfigurationProperties(prefix = "order.timeout")
public class OrderTimeoutProperties {

    // Store only mutable configuration fields to avoid rebuilding business Beans
    private Integer read = 200;

    public Integer getRead() { return read; }
    public void setRead(Integer read) { this.read = read; }
}

This example moves hot-update responsibility into a dedicated configuration carrier and avoids replacing the entire business Bean at runtime.

@Service
public class OrderService {

    private final OrderTimeoutProperties properties;

    public OrderService(OrderTimeoutProperties properties) {
        this.properties = properties;
    }

    public int currentTimeout() {
        return properties.getRead(); // The business class only reads the latest configuration value
    }
}

This example shows that the business object stays as a stable singleton and simply reads real-time values from the configuration object, so the request path does not pay reconstruction cost.

Migration should prioritize Controllers first and then core Services

If you plan to fix this class of issues, prioritize by blast radius. First, remove @RefreshScope from all Controllers because they sit directly on the traffic entry path. Second, remove it from high-frequency core Services.

Then move dynamic parameters into unified XxxProperties configuration classes. Finally, validate the result with load testing to confirm that scale-up latency spikes, GC frequency, and configuration-release jitter disappear. In practice, this optimization often delivers visible gains quickly.

Read/write lock and blocking risk illustration AI Visual Insight: This image focuses on lock control during get/destroy operations and shows how concurrent requests can be serialized when the configuration cache is invalidated or when the first instance is being created. That serialization effect is a key amplifier behind startup jitter and configuration-release jitter.

FAQ

FAQ 1: Is @RefreshScope completely unusable?

No. It is suitable for a small number of Beans with clear boundaries where a brief reconstruction cost is acceptable. It is not suitable for Controllers, core Services, or objects on high-concurrency execution paths.

FAQ 2: Why can it also trigger Full GC?

Once requests are blocked, large numbers of thread stacks, temporary objects, serialization context, and response buffers can accumulate. Pressure on the old generation increases, and that eventually manifests as frequent GC. GC is the result, not the root cause.

FAQ 3: How can I quickly audit whether my project is at risk?

Search globally for @RefreshScope. Focus on Controllers, Services, Feign entry adapters, and high-frequency components. Then correlate that with scale-out monitoring to see whether request latency, thread waiting time, and GC peaks appear at the same time.

Summary

In Spring Cloud and Nacos environments, misusing @RefreshScope on Controllers and Services can cause API timeouts, startup jitter, and frequent Full GC after scaling because of lazy initialization, scope-level read/write locks, and the refreshAll() reconstruction mechanism. The practical fix is to replace this pattern with @ConfigurationProperties-based configuration objects.