Thread.sleep() inside Workflow
- Engine
- Temporal Java SDK
- Since
- 0.2.0
- Auto Fix
- Not Available
- Category Range
- WG001–WG099
Description
A Temporal workflow implementation — or any code reachable from a workflow's entry
point, however many method calls away — calls Thread.sleep(...).
Why This Matters
A Temporal worker executes many workflow tasks concurrently on a small, shared pool of
threads. Thread.sleep() blocks whichever worker thread happens to be executing the
workflow task for its entire duration, holding that thread hostage instead of yielding it
back to the pool. At any real scale this starves the worker of capacity to make progress
on other workflows.
It's also the wrong tool for the job on its own terms: Temporal's programming model expects a workflow to express "wait" as a durable, replayable timer, not as a blocked thread. A blocked thread has no representation in workflow history — there is nothing for replay to reconstruct — so relying on it, rather than a timer Temporal itself tracks, works against how the workflow's progress is meant to be recorded and resumed.
io.temporal.workflow.Workflow.sleep(Duration) is the correct replacement: it registers
a durable timer recorded in workflow history, does not block the worker thread, and
resumes correctly on replay regardless of how much real wall-clock time has passed.
Violation Example
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() throws InterruptedException {
Thread.sleep(5000);
}
}This is flagged even when the call is several methods away from the workflow entry point:
public class PaymentWorkflowImpl implements PaymentWorkflow {
private final PaymentService paymentService = new PaymentService();
@Override
public void processPayment() {
// WG002 still fires: WoGu's call-graph analysis follows this call into
// PaymentService.waitForSettlement() and finds Thread.sleep() inside it.
paymentService.waitForSettlement();
}
}
class PaymentService {
void waitForSettlement() throws Exception {
Thread.sleep(5000);
}
}The report renders the full path from the workflow entry point down to the blocking call:
PaymentWorkflow.process()
↓
PaymentService.waitForSettlement()
↓
Thread.sleep()Compliant Example
import io.temporal.workflow.Workflow;
import java.time.Duration;
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
Workflow.sleep(Duration.ofSeconds(5));
}
}Recommended Fix
Replace Thread.sleep(millis) with Workflow.sleep(Duration.ofSeconds(...)) (or the
equivalent Duration for the delay you need). Workflow.sleep() does not block the
worker thread and is backed by a durable timer that survives worker restarts and replays
correctly.
If you need to block inside an Activity rather than workflow code, this rule doesn't
apply — activities are not replayed and are expected to run on their own thread pool, so
Thread.sleep() is an acceptable (if still often better replaced by a proper timeout
mechanism) choice there. WG002 only flags code reachable from a workflow's own entry
point (see Call Graph Analysis below for what "reachable" means).
False Positives
WG002 uses the same call-graph analysis as WG001: starting from a workflow's
entry-point method(s), it follows every method call that can be resolved to another
method's source within the same project, however many hops deep, looking for
Thread.sleep(...) along the way.
As with WG001, this analysis intentionally stops, without reporting anything past that
point, when it reaches a call it cannot resolve to source it can see — most notably, a
call to an Activity made through the activity's interface (the normal way Temporal
activities are invoked): WoGu resolves such a call against the interface's method
declaration, which has no body to look inside, so the activity's real implementation
(and anything it does, including its own Thread.sleep() calls) is never reached. A call
into a compiled dependency, or one resolved only through reflection or dynamic dispatch,
is an equally intentional stopping point.
This means WG002 can produce false negatives (a real violation hidden behind a call
it can't see into) but is designed to avoid false positives — it will not flag a call
just because it looks superficially similar to Thread.sleep(); it must resolve to that
exact method on java.lang.Thread (an explicit import is never required, since
java.lang classes are always in scope, but an import of a different class also named
Thread correctly prevents a false match). If you believe WG002 is flagging or missing
something incorrectly, please open an issue with a minimal reproduction.
Found an issue with this rule's detection? Open an issue with a minimal reproduction.