What are the risks of propagating RequestContextHolder / request-scoped beans across the async thread boundary, and how would you do it safely?
answer
- RequestAttributes thread-local -> ServletRequestAttributes
- container recycles request/response objects
- task may outlive request -> IllegalStateException / stale data
- extract immutable values, pass explicitly
- resetRequestAttributes() in finally if you must propagate
basics
~20 sThe HttpServletRequest and request-scoped beans live only for the request; once it completes the request/response may be recycled and its attributes cleared. Copying RequestContextHolder to a pool thread risks touching a dead request. Safer: extract the plain values you need before submitting, and pass them explicitly.
solid answer
~50 sRequestContextHolder stores the current RequestAttributes (the HttpServletRequest) in a thread-local; request scope resolves beans against it. Propagating it to an async pool thread is dangerous because the Servlet container may recycle the request/response objects (and the container may return before/independently of your async task), so the pool thread can read a cleared or reused request — stale or wrong data, or IllegalStateException. RequestContextHolder.setRequestAttributes(attrs, true) makes attributes inheritable, but that neither guarantees the request is still alive nor helps a reused pool thread. The robust approach: on the request thread, extract the specific immutable values you need (locale, a header, a userId) and pass them into the async work explicitly, rather than propagating the live request. If you must propagate diagnostic/security context, use dedicated mechanisms (MDC/SecurityContext copies, Micrometer accessors) — not the raw request object. Always clean up in finally.
code
java · 17 lines// PREFERRED: decouple async work from the request lifecycle.
@Service
public class ReportService {
private final AsyncTaskExecutor executor;
ReportService(AsyncTaskExecutor executor) { this.executor = executor; }
// Called on the request thread with already-extracted, immutable values.
public void generateAsync(java.util.Locale locale, String correlationId) {
executor.submit(() -> {
// no RequestContextHolder access here — nothing to recycle out from under us
buildReport(locale, correlationId);
});
}
private void buildReport(java.util.Locale locale, String correlationId) { /* ... */ }
}go deeper
Know request-scoped data isn't visible on async threads.
Understand the request may end before the task and that copying the request is risky.
Prescribe extract-and-pass and explain container request recycling.
Weigh propagation strategies against Servlet lifecycle, reused-thread leakage, and testability; steer teams to decouple async work from the request object entirely.
## What request scope depends on Spring's **request scope** and **`RequestContextHolder`** are backed by a thread-local `RequestAttributes` pointing at the current `HttpServletRequest` (via `ServletRequestAttributes`). Request-scoped beans (`@Scope("request")`) resolve their instance from these attributes. All of this is bound by `RequestContextFilter`/`DispatcherServlet` to the **container thread**, and is torn down when the request completes. ## Why crossing the async boundary is risky 1. **Lifecycle mismatch.** The pool task may run *after* the HTTP request has completed. Servlet containers **pool and recycle** `HttpServletRequest`/`HttpServletResponse` objects; once recycled, reading them throws `IllegalStateException` or returns data belonging to a *different* request. Propagating the live `RequestAttributes` to a worker means it may dereference a dead/reused request. 2. **`RequestContextHolder` is thread-local.** By default the worker sees `null` attributes, so request-scoped beans fail to resolve. 3. **Inheritable doesn't save you.** `RequestContextHolder.setRequestAttributes(attributes, /*inheritable*/ true)` uses an `InheritableThreadLocal`, so **newly spawned** child threads inherit it — but pool threads are pre-created and reused, and even when inherited the underlying request may already be recycled. So inheritability solves neither the lifecycle nor the reuse problem. 4. **Reused-thread leakage.** As with MDC/security, a set-without-clear leaks request attributes into the next pooled task. ## The safe design **Extract, then pass.** On the request thread, before submitting, pull out the specific *immutable values* the async work needs: ```java @GetMapping("/report") public void generate() { HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); Locale locale = req.getLocale(); String correlationId = req.getHeader("X-Correlation-Id"); // pass plain values into the async work; do NOT hand over the request reportService.generateAsync(locale, correlationId); } ``` This decouples the background work from the request lifecycle entirely — the safest and most testable option. **If you truly must propagate request attributes** (e.g., a library reads request-scoped beans), do it only for tasks that are guaranteed to run **while the request is still open** (i.e., the request thread blocks on the result), and use a `TaskDecorator` that captures `RequestContextHolder.getRequestAttributes()` at submit and clears it in `finally`: ```java public Runnable decorate(Runnable runnable) { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); return () -> { if (attrs != null) RequestContextHolder.setRequestAttributes(attrs); try { runnable.run(); } finally { RequestContextHolder.resetRequestAttributes(); } }; } ``` Even then, only header/attribute reads that don't touch recyclable stream state are safe. ## For diagnostics/security, use the right tool Don't smuggle a `requestId` or the principal via the raw request. Propagate **MDC** and **SecurityContext** with their dedicated mechanisms (copies, `DelegatingSecurityContext*`, or Micrometer accessors). Those values are plain data, not lifecycle-bound container objects. ## Summary of trade-offs - **Extract-and-pass:** safest, no lifecycle coupling, fully testable — preferred default. - **Propagate RequestAttributes via decorator:** only when the request outlives the task; requires strict cleanup; fragile. - **InheritableThreadLocal request attributes:** avoid for pools — false sense of safety, leaks, and dead-request hazards.
- Why doesn't RequestContextHolder.setRequestAttributes(attrs, true) make async request-scope safe?The inheritable flag only copies attributes to threads spawned from the current one — pool threads are reused, not freshly spawned — and even when inherited, the container may have recycled the underlying HttpServletRequest, so reads hit a dead/reused request.
- When is propagating RequestAttributes to a worker thread actually acceptable?Only when the request thread blocks until the async task completes, so the request is guaranteed still open, and you clear via resetRequestAttributes() in finally. Even then, avoid reading recyclable stream state; prefer extracting plain values.
saying these in an interview costs you the question
- Handing the live HttpServletRequest to a pool thread that may outlive the request.
- Believing the inheritable RequestContextHolder flag makes request scope safe for pools.
- Reading request-scoped beans on an async thread without guaranteeing the request is still open.
- Using the raw request to carry a requestId instead of MDC/context-propagation.