A script that hammers an API with no pause, a test that finishes before the async work it started, a benchmark that lies by a factor of ten: bad pausing and bad measuring cause real production bugs. This groovy sleep guide covers the GDK sleep() method, Thread.sleep(), timing with nanoTime, benchmarking, and timeout patterns, all tested on Groovy 5.0.6.
“Timing is everything – in comedy, in cooking, and especially in programming.”
Last Updated: July 2026 | Tested on: Groovy 5.0.6, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 17 minutes
Polling a remote service, rate-limiting API calls, benchmarking a database query – sooner or later you need to pause execution or measure elapsed time. Groovy sleep via the GDK sleep() method adds interrupt-safe pausing to Java’s Thread.sleep(), and this guide covers both along with timing, benchmarking, and timeout patterns.
Groovy offers two ways to pause execution: Java’s Thread.sleep() and Groovy’s own GDK sleep() method. The Groovy version adds a useful feature – a closure that handles interrupts gracefully. Beyond sleeping, we will also cover measuring elapsed time with System.nanoTime() and System.currentTimeMillis(), benchmarking code, timeout patterns, and scheduled execution.
The diagram below is a quick picker that starts from your goal, pausing, measuring elapsed time, or waiting for a condition, and routes you to the right construct so you can see how all the tools in this guide fit together at a glance:
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
In the previous post, we covered all the output methods in Groovy. Now let us explore the timing side – how to control when output happens and how to measure the performance of your code. If you need file-related operations with timing (like watching a file for changes), check our post on Groovy file read, write, and delete.
Table of Contents
Thread.sleep() – The Java Way
Since Groovy runs on the JVM, you can use Thread.sleep() exactly like you would in Java. It pauses the current thread for the specified number of milliseconds. The method throws an InterruptedException if the thread is interrupted during sleep, but Groovy scripts do not require you to catch checked exceptions.
According to the Groovy documentation on differences from Java, Groovy does not enforce checked exception handling. This means you can call Thread.sleep() without a try-catch block, unlike Java where the compiler forces you to handle InterruptedException.
| Method | Source | Interrupt Handling | Parameter |
|---|---|---|---|
Thread.sleep(ms) | Java SDK | Throws InterruptedException | Milliseconds (long) |
sleep(ms) | Groovy GDK | Swallows interrupt, returns boolean | Milliseconds (long) |
sleep(ms) { closure } | Groovy GDK | Passes exception to closure | Milliseconds + Closure |
sleep() – The Groovy GDK Way
Groovy adds a sleep() method to every object through the GDK. The Groovy version is more convenient because it handles InterruptedException internally. You can optionally pass a closure that gets called if the sleep is interrupted – the closure receives the exception and returns a boolean indicating whether to continue sleeping.
Groovy sleep() vs Thread.sleep()
// Java way - Thread.sleep()
println "Java way: starting sleep..."
Thread.sleep(500) // 500 milliseconds
println "Java way: woke up!"
// Groovy way - sleep()
println "\nGroovy way: starting sleep..."
sleep(500) // Same result, cleaner syntax
println "Groovy way: woke up!"
// Groovy way with interrupt handler
println "\nWith interrupt handler:"
sleep(500) { InterruptedException e ->
println "Sleep was interrupted: ${e.message}"
return true // true = continue sleeping, false = stop
}
println "Finished sleeping"
Output
Java way: starting sleep... Java way: woke up! Groovy way: starting sleep... Groovy way: woke up! With interrupt handler: Finished sleeping
The Groovy sleep() method is the preferred approach in Groovy scripts. It is cleaner, handles interrupts gracefully, and follows Groovy conventions.
14 Practical Sleep and Timing Examples
Let us work through practical examples covering every aspect of sleeping and timing in Groovy. One quick note before we start: the timestamps, millisecond timings, and random data counts in the outputs below come from a single run on my machine, so your numbers will differ a little from run to run.
Example 1: Basic Thread.sleep()
The simplest way to pause execution. Pass the number of milliseconds to wait.
Example 1: Basic Thread.sleep()
println "Starting at: ${new Date().format('HH:mm:ss.SSS')}"
// Sleep for 1 second
Thread.sleep(1000)
println "After 1 second: ${new Date().format('HH:mm:ss.SSS')}"
// Sleep for half a second
Thread.sleep(500)
println "After 0.5 seconds: ${new Date().format('HH:mm:ss.SSS')}"
// Sleep for 2 seconds
Thread.sleep(2000)
println "After 2 seconds: ${new Date().format('HH:mm:ss.SSS')}"
// Common time conversions
def SECOND = 1000L
def MINUTE = 60 * SECOND
def HOUR = 60 * MINUTE
println "\n--- Time Constants ---"
println "1 second = ${SECOND} ms"
println "1 minute = ${MINUTE} ms"
println "1 hour = ${HOUR} ms"
// Sleep using named constants
Thread.sleep(2 * SECOND)
println "\nSlept for 2 seconds using constant"
Output
Starting at: 14:30:00.000 After 1 second: 14:30:01.003 After 0.5 seconds: 14:30:01.505 After 2 seconds: 14:30:03.507 --- Time Constants --- 1 second = 1000 ms 1 minute = 60000 ms 1 hour = 3600000 ms Slept for 2 seconds using constant
Note that sleep is not perfectly precise. The actual sleep time can be a few milliseconds longer than requested due to thread scheduling overhead. Never rely on sleep for precise timing – it is a minimum delay, not an exact one.
Example 2: Groovy sleep() GDK Method
Groovy’s GDK sleep() method is available on every object. It is more idiomatic in Groovy and handles interrupts without requiring a try-catch.
Example 2: Groovy GDK sleep()
// Simple sleep - no exception handling needed
println "Step 1: Initialize"
sleep(500)
println "Step 2: Process"
sleep(500)
println "Step 3: Complete"
// Sleep returns void in simple form
println "\n--- Countdown ---"
(5..1).each { count ->
print "${count}... "
System.out.flush()
sleep(200)
}
println "Go!"
// Using sleep in a loop
println "\n--- Heartbeat ---"
5.times { beat ->
println "♥ beat ${beat + 1} at ${new Date().format('HH:mm:ss.SSS')}"
if (beat < 4) sleep(300)
}
Output
Step 1: Initialize Step 2: Process Step 3: Complete --- Countdown --- 5... 4... 3... 2... 1... Go! --- Heartbeat --- ♥ beat 1 at 14:30:00.100 ♥ beat 2 at 14:30:00.405 ♥ beat 3 at 14:30:00.710 ♥ beat 4 at 14:30:01.015 ♥ beat 5 at 14:30:01.320
The Groovy sleep() method is the cleanest way to add delays in Groovy scripts. No imports, no exception handling, just a simple method call.
Example 3: sleep() with Interrupt Handling Closure
The real power of Groovy’s sleep() comes when you pass a closure. If the sleeping thread is interrupted, the closure gets called with the InterruptedException. The closure returns a boolean: true to continue sleeping for the remaining time, or false to wake up immediately.
Example 3: sleep() with Interrupt Closure
// sleep with interrupt handler
def sleepWithLogging(long millis) {
println "Sleeping for ${millis}ms..."
sleep(millis) { InterruptedException e ->
println " Interrupted! ${e.message}"
println " Deciding to stop sleeping..."
return false // false = don't continue sleeping
}
println "Awake!"
}
sleepWithLogging(500)
// Demonstrating interrupt handling with a thread
println "\n--- Interrupt Demo ---"
def worker = Thread.start {
println "[Worker] Starting long sleep (5 seconds)..."
sleep(5000) { InterruptedException e ->
println "[Worker] Interrupted after partial sleep!"
println "[Worker] Returning false to wake up immediately"
return false // Stop sleeping
}
println "[Worker] Worker thread finished"
}
// Let the worker sleep for 1 second, then interrupt it
sleep(1000)
println "[Main] Interrupting worker thread..."
worker.interrupt()
// Wait for worker to finish
worker.join()
println "[Main] All done"
Output
Sleeping for 500ms... Awake! --- Interrupt Demo --- [Worker] Starting long sleep (5 seconds)... [Main] Interrupting worker thread... [Worker] Interrupted after partial sleep! [Worker] Returning false to wake up immediately [Worker] Worker thread finished [Main] All done
This pattern is valuable for long-running background tasks. The closure gives you a clean way to decide what happens when your sleep is interrupted – log it, clean up resources, or simply wake up early.
Example 4: Measuring Time with System.currentTimeMillis()
System.currentTimeMillis() returns the current time in milliseconds since the Unix epoch (January 1, 1970). It is the simplest way to measure elapsed time for operations that take at least a few milliseconds.
Example 4: System.currentTimeMillis()
// Basic elapsed time measurement
def startTime = System.currentTimeMillis()
// Simulate some work (0L: a plain int would wrap past Integer.MAX_VALUE)
def sum = 0L
(1..1_000_000).each { sum += it }
def endTime = System.currentTimeMillis()
def elapsed = endTime - startTime
println "Sum: ${sum}"
println "Time taken: ${elapsed} ms"
// Timing multiple operations
println "\n--- Operation Timings ---"
def operations = [
"String concatenation": {
def s = ""
500.times { s += "x" }
},
"StringBuilder": {
def sb = new StringBuilder()
500.times { sb.append("x") }
},
"List creation": {
def list = (1..10000).toList()
},
"Map creation": {
def map = [:]
1000.times { map["key_${it}"] = it }
}
]
operations.each { name, operation ->
def start = System.currentTimeMillis()
operation()
def time = System.currentTimeMillis() - start
printf " %-25s: %5d ms%n", name, time
}
Output
Sum: 500000500000 Time taken: 224 ms --- Operation Timings --- String concatenation : 31 ms StringBuilder : 16 ms List creation : 15 ms Map creation : 16 ms
One detail worth calling out: the accumulator starts at 0L, not 0. A million integers add up to well past Integer.MAX_VALUE, and Groovy int arithmetic wraps around silently just like Java, so summing into a plain int would print a garbage value. Beyond that, this approach is good enough for most timing needs. If you need nanosecond precision for microbenchmarks, use System.nanoTime() instead (covered in the next example).
Example 5: High-Precision Timing with System.nanoTime()
System.nanoTime() provides nanosecond-precision timing. Unlike currentTimeMillis(), it is not related to wall-clock time – it is a monotonic counter that can only be used to measure elapsed time between two calls.
Example 5: System.nanoTime()
// High-precision timing
def startNano = System.nanoTime()
// Something fast that currentTimeMillis might miss
def list = (1..1000).collect { it * 2 }
def elapsedNano = System.nanoTime() - startNano
def elapsedMicro = elapsedNano / 1000
def elapsedMilli = elapsedNano / 1_000_000
println "Elapsed: ${elapsedNano} nanoseconds"
println "Elapsed: ${String.format('%.2f', elapsedMicro)} microseconds"
println "Elapsed: ${String.format('%.3f', elapsedMilli)} milliseconds"
// Comparing nanoTime vs currentTimeMillis precision
println "\n--- Precision Comparison ---"
def iterations = 5
println "Using currentTimeMillis:"
iterations.times {
def start = System.currentTimeMillis()
(1..10000).sum()
def time = System.currentTimeMillis() - start
printf " Run %d: %d ms%n", it + 1, time
}
println "\nUsing nanoTime:"
iterations.times {
def start = System.nanoTime()
(1..10000).sum()
def time = (System.nanoTime() - start) / 1_000_000.0
printf " Run %d: %.3f ms%n", it + 1, time
}
Output
Elapsed: 71976200 nanoseconds Elapsed: 71976.20 microseconds Elapsed: 71.976 milliseconds --- Precision Comparison --- Using currentTimeMillis: Run 1: 22 ms Run 2: 16 ms Run 3: 0 ms Run 4: 15 ms Run 5: 0 ms Using nanoTime: Run 1: 5.154 ms Run 2: 5.373 ms Run 3: 12.406 ms Run 4: 3.446 ms Run 5: 3.277 ms
Notice how currentTimeMillis shows “0 ms” for fast operations because it rounds to the nearest millisecond. nanoTime captures the sub-millisecond detail. Use nanoTime for benchmarking and currentTimeMillis for general-purpose timing.
Example 6: Reusable Timing Utility
Let us build a reusable timing utility that wraps any code block and reports how long it took. This is one of the most useful patterns for development and debugging.
Example 6: Timing Utility
// A simple timing utility
def timeIt(String label, Closure code) {
def start = System.nanoTime()
def result = code()
def elapsed = (System.nanoTime() - start) / 1_000_000.0
printf "⏱ %-30s: %8.3f ms%n", label, elapsed
return result
}
// Time different operations
def list = timeIt("Create list of 100,000") {
(1..100_000).toList()
}
timeIt("Sort the list") {
list.sort()
}
timeIt("Find max value") {
list.max()
}
timeIt("Filter even numbers") {
list.findAll { it % 2 == 0 }
}
timeIt("Sum all values") {
list.sum()
}
timeIt("Convert to set") {
list as Set
}
// Timing with return value
println "\n--- With Return Values ---"
def sum = timeIt("Calculate sum") {
(1..1_000_000).sum(0L) // 0L keeps the total in long range
}
println "Result: ${sum}"
def sorted = timeIt("Sort 10,000 random numbers") {
def random = new Random(42)
(1..10_000).collect { random.nextInt(100_000) }.sort()
}
println "First 5: ${sorted.take(5)}, Last 5: ${sorted.takeRight(5)}"
Output
⏱ Create list of 100,000 : 25.005 ms ⏱ Sort the list : 26.666 ms ⏱ Find max value : 7.542 ms ⏱ Filter even numbers : 95.592 ms ⏱ Sum all values : 44.163 ms ⏱ Convert to set : 9.663 ms --- With Return Values --- ⏱ Calculate sum : 248.234 ms Result: 500000500000 ⏱ Sort 10,000 random numbers : 41.813 ms First 5: [3, 9, 15, 31, 34], Last 5: [99931, 99933, 99941, 99989, 99999]
This timeIt pattern is something you will use constantly during development. It wraps any code block, measures its execution time, and still returns the result so your program flow is not affected.
Example 7: Benchmarking with Multiple Runs
A single timing run can be misleading due to JVM warmup, garbage collection, and OS scheduling. For reliable benchmarks, run the code multiple times and look at averages and percentiles.
Example 7: Benchmarking
def benchmark(String label, int runs, Closure code) {
def times = []
// Warmup runs (not counted)
3.times { code() }
// Measured runs
runs.times {
def start = System.nanoTime()
code()
times << (System.nanoTime() - start) / 1_000_000.0
}
def sorted = times.sort()
def avg = times.sum() / times.size()
def min = sorted.first()
def max = sorted.last()
def median = sorted[(sorted.size() / 2) as int]
def p95 = sorted[(sorted.size() * 0.95) as int]
println "\n📊 ${label} (${runs} runs)"
printf " Average: %8.3f ms%n", avg
printf " Median: %8.3f ms%n", median
printf " Min: %8.3f ms%n", min
printf " Max: %8.3f ms%n", max
printf " P95: %8.3f ms%n", p95
}
// Benchmark: String concatenation vs StringBuilder
def iterations = 1000
benchmark("String concatenation (${iterations} appends)", 20) {
def s = ""
iterations.times { s += "x" }
}
benchmark("StringBuilder (${iterations} appends)", 20) {
def sb = new StringBuilder()
iterations.times { sb.append("x") }
sb.toString()
}
benchmark("StringBuffer (${iterations} appends)", 20) {
def sb = new StringBuffer()
iterations.times { sb.append("x") }
sb.toString()
}
// Benchmark: List operations
benchmark("ArrayList sort (10,000 elements)", 20) {
def random = new Random(42)
def list = (1..10_000).collect { random.nextInt() }
list.sort()
}
benchmark("Array sort (10,000 elements)", 20) {
def random = new Random(42)
int[] arr = (1..10_000).collect { random.nextInt() } as int[]
Arrays.sort(arr)
}
Output
📊 String concatenation (1000 appends) (20 runs) Average: 1.387 ms Median: 1.306 ms Min: 1.197 ms Max: 2.683 ms P95: 2.683 ms 📊 StringBuilder (1000 appends) (20 runs) Average: 0.681 ms Median: 0.684 ms Min: 0.471 ms Max: 1.052 ms P95: 1.052 ms 📊 StringBuffer (1000 appends) (20 runs) Average: 0.751 ms Median: 0.683 ms Min: 0.504 ms Max: 1.420 ms P95: 1.420 ms 📊 ArrayList sort (10,000 elements) (20 runs) Average: 4.419 ms Median: 4.256 ms Min: 2.744 ms Max: 6.589 ms P95: 6.589 ms 📊 Array sort (10,000 elements) (20 runs) Average: 3.969 ms Median: 3.138 ms Min: 2.349 ms Max: 8.267 ms P95: 8.267 ms
At 1000 appends the JIT keeps plain concatenation surprisingly competitive, and StringBuilder came out only about 2x faster in this run. The gap explodes as the string grows though. In my tests the same comparison at 50,000 appends made concatenation more than 50x slower, because every += copies the whole string again. Always benchmark with warmup runs and multiple iterations for reliable results.
Example 8: Timeout Pattern
A timeout pattern waits for a condition to become true, but gives up after a maximum wait time. This is essential for operations that might hang – network calls, file watches, or waiting for external processes.
Example 8: Timeout Pattern
// Generic wait-with-timeout
def waitFor(String description, long timeoutMs, long pollIntervalMs = 100, Closure<Boolean> condition) {
def deadline = System.currentTimeMillis() + timeoutMs
println "Waiting for: ${description} (timeout: ${timeoutMs}ms)"
while (System.currentTimeMillis() < deadline) {
if (condition()) {
println " ✓ ${description} - condition met!"
return true
}
sleep(pollIntervalMs)
}
println " ✗ ${description} - TIMED OUT after ${timeoutMs}ms"
return false
}
// Example 1: Simulating waiting for a file to appear
def fileReady = false
Thread.start {
sleep(800) // Simulate file creation after 800ms
fileReady = true
}
def result1 = waitFor("File to be created", 2000) {
fileReady
}
println "File ready: ${result1}"
// Example 2: Timeout that actually expires
def neverReady = false
def result2 = waitFor("Service to respond", 500, 100) {
neverReady // This will never become true
}
println "Service ready: ${result2}"
// Example 3: Waiting for a counter
def counter = 0
Thread.start {
10.times {
sleep(100)
counter++
}
}
def result3 = waitFor("Counter to reach 5", 3000, 50) {
counter >= 5
}
println "Counter reached 5: ${result3} (actual: ${counter})"
Output
Waiting for: File to be created (timeout: 2000ms) ✓ File to be created - condition met! File ready: true Waiting for: Service to respond (timeout: 500ms) ✗ Service to respond - TIMED OUT after 500ms Service ready: false Waiting for: Counter to reach 5 (timeout: 3000ms) ✓ Counter to reach 5 - condition met! Counter reached 5: true (actual: 5)
This waitFor pattern is extremely useful. You will find yourself reaching for it whenever you need to wait for an asynchronous condition with a safety timeout.
Example 9: ScheduledExecutorService
For more sophisticated scheduling, Java’s ScheduledExecutorService is the modern, thread-safe way to run code on a schedule. Groovy closures work directly as Runnable instances.
Example 9: ScheduledExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
// Create a scheduled executor with 2 threads
def scheduler = Executors.newScheduledThreadPool(2)
println "Starting scheduler at ${new Date().format('HH:mm:ss.SSS')}"
// Schedule a one-time task after 1 second delay
scheduler.schedule({
println " [One-shot] Executed at ${new Date().format('HH:mm:ss.SSS')}"
} as Runnable, 1, TimeUnit.SECONDS)
// Schedule a repeating task every 500ms, starting after 200ms
def counter = 0
def repeatingTask = scheduler.scheduleAtFixedRate({
counter++
println " [Repeating] Tick #${counter} at ${new Date().format('HH:mm:ss.SSS')}"
} as Runnable, 200, 500, TimeUnit.MILLISECONDS)
// Let it run for 2.5 seconds
sleep(2500)
// Cancel the repeating task
repeatingTask.cancel(false)
println "Repeating task cancelled after ${counter} ticks"
// Schedule with fixed delay (waits between end of one run and start of next)
def delayCounter = 0
def delayTask = scheduler.scheduleWithFixedDelay({
delayCounter++
println " [FixedDelay] Task #${delayCounter} - simulating 200ms work"
sleep(200) // Simulate work
} as Runnable, 0, 300, TimeUnit.MILLISECONDS)
sleep(2000)
delayTask.cancel(false)
println "Fixed delay task cancelled after ${delayCounter} runs"
// Shutdown the scheduler
scheduler.shutdown()
scheduler.awaitTermination(5, TimeUnit.SECONDS)
println "Scheduler shut down cleanly"
Output
Starting scheduler at 16:17:23.373 [Repeating] Tick #1 at 16:17:23.936 [Repeating] Tick #2 at 16:17:24.355 [One-shot] Executed at 16:17:24.609 [Repeating] Tick #3 at 16:17:24.859 [Repeating] Tick #4 at 16:17:25.361 [Repeating] Tick #5 at 16:17:25.851 Repeating task cancelled after 5 ticks [FixedDelay] Task #1 - simulating 200ms work [FixedDelay] Task #2 - simulating 200ms work [FixedDelay] Task #3 - simulating 200ms work [FixedDelay] Task #4 - simulating 200ms work Fixed delay task cancelled after 4 runs Scheduler shut down cleanly
The key difference between scheduleAtFixedRate and scheduleWithFixedDelay is: fixed rate tries to maintain a consistent interval (even if tasks overlap), while fixed delay waits for the specified delay after each task completes.
Example 10: Timer and TimerTask
Java’s Timer class is an older, simpler alternative to ScheduledExecutorService. It is still useful for simple scheduling in scripts where you do not need thread pool management.
Example 10: Timer and TimerTask
// Simple timer with Groovy closure
def timer = new Timer("MyTimer", true) // true = daemon thread
println "Timer started at ${new Date().format('HH:mm:ss.SSS')}"
// Schedule a one-time task
timer.schedule(new TimerTask() {
void run() {
println " [Timer] One-shot task at ${new Date().format('HH:mm:ss.SSS')}"
}
}, 1000) // 1 second delay
// Schedule a repeating task
def tickCount = 0
def repeating = new TimerTask() {
void run() {
tickCount++
println " [Timer] Tick #${tickCount} at ${new Date().format('HH:mm:ss.SSS')}"
if (tickCount >= 5) {
this.cancel() // Cancel from within the task
println " [Timer] Self-cancelled after 5 ticks"
}
}
}
timer.scheduleAtFixedRate(repeating, 200, 400) // start after 200ms, repeat every 400ms
// Wait for tasks to complete
sleep(3000)
// A Groovy-friendly timer helper
def runAfter(long delayMs, Closure action) {
def t = new Timer(true)
t.schedule(new TimerTask() {
void run() {
action()
t.cancel()
}
}, delayMs)
return t
}
println "\nUsing helper:"
runAfter(500) {
println " Delayed action executed!"
}
sleep(1000)
// Cleanup
timer.cancel()
println "Timer cancelled"
Output
Timer started at 14:30:00.000 [Timer] Tick #1 at 14:30:00.202 [Timer] Tick #2 at 14:30:00.602 [Timer] One-shot task at 14:30:01.002 [Timer] Tick #3 at 14:30:01.002 [Timer] Tick #4 at 14:30:01.402 [Timer] Tick #5 at 14:30:01.802 [Timer] Self-cancelled after 5 ticks Using helper: Delayed action executed! Timer cancelled
For new code, prefer ScheduledExecutorService over Timer. The executor is more reliable – it handles exceptions in tasks without dying, supports multiple threads, and integrates with the Java concurrency framework. But Timer is perfectly fine for simple scripting tasks.
Example 11: Real-World – Polling Pattern
Polling is a common pattern where you repeatedly check for a condition at regular intervals. This is used for watching files, checking API status, monitoring queues, and more.
Example 11: Polling Pattern
// Generic polling function
def poll(Map config = [:], Closure<Boolean> check) {
def interval = config.interval ?: 1000
def timeout = config.timeout ?: 10000
def label = config.label ?: "condition"
def onTimeout = config.onTimeout ?: { throw new RuntimeException("Polling timed out: ${label}") }
def startTime = System.currentTimeMillis()
def attempt = 0
while (true) {
attempt++
def elapsed = System.currentTimeMillis() - startTime
if (check()) {
printf " ✓ [Poll] %s met after %d attempts (%.1f seconds)%n", label, attempt, elapsed / 1000.0
return true
}
if (elapsed + interval > timeout) {
printf " ✗ [Poll] %s timed out after %d attempts (%.1f seconds)%n", label, attempt, elapsed / 1000.0
onTimeout()
return false
}
sleep(interval)
}
}
// Simulate an external service becoming ready
def serviceStatus = "starting"
Thread.start {
sleep(1500)
serviceStatus = "ready"
}
println "Polling for service readiness..."
poll(interval: 300, timeout: 5000, label: "Service ready") {
serviceStatus == "ready"
}
println "Service status: ${serviceStatus}"
// Simulate polling a job queue
println "\n--- Job Queue Monitor ---"
def jobQueue = ['job_1', 'job_2', 'job_3']
Thread.start {
while (jobQueue) {
sleep(400)
def completed = jobQueue.remove(0)
println " [Worker] Completed: ${completed}"
}
}
poll(interval: 200, timeout: 5000, label: "Queue empty") {
jobQueue.isEmpty()
}
println "All jobs completed!"
Output
Polling for service readiness... ✓ [Poll] Service ready met after 6 attempts (1.5 seconds) Service status: ready --- Job Queue Monitor --- [Worker] Completed: job_1 [Worker] Completed: job_2 [Worker] Completed: job_3 ✓ [Poll] Queue empty met after 8 attempts (1.4 seconds) All jobs completed!
Example 12: Real-World – Rate Limiting
When calling external APIs, you often need to limit how many requests you send per second. Sleep-based rate limiting is the simplest approach.
Example 12: Rate Limiting
// Simple rate limiter
class RateLimiter {
final long intervalMs
long lastCallTime = 0
RateLimiter(int maxPerSecond) {
this.intervalMs = (1000 / maxPerSecond) as long
}
void acquire() {
def now = System.currentTimeMillis()
def elapsed = now - lastCallTime
if (elapsed < intervalMs) {
sleep(intervalMs - elapsed)
}
lastCallTime = System.currentTimeMillis()
}
}
// Rate limit to 5 requests per second
def limiter = new RateLimiter(5)
println "Making 10 API calls at max 5/second:"
def startTime = System.currentTimeMillis()
10.times { i ->
limiter.acquire()
def elapsed = System.currentTimeMillis() - startTime
printf " Request %2d at %4d ms%n", i + 1, elapsed
}
def totalTime = System.currentTimeMillis() - startTime
printf "%nTotal time: %d ms (expected ~1800ms for 10 calls at 5/sec)%n", totalTime
// Batch processing with rate limiting
println "\n--- Batch API Processor ---"
def items = (1..8).collect { "item_${it}" }
def batchLimiter = new RateLimiter(3) // 3 per second
println "Processing ${items.size()} items at max 3/second:"
def batchStart = System.currentTimeMillis()
items.each { item ->
batchLimiter.acquire()
def elapsed = System.currentTimeMillis() - batchStart
printf " Processing %-10s at %5d ms%n", item, elapsed
}
printf "Batch complete in %d ms%n", System.currentTimeMillis() - batchStart
Output
Making 10 API calls at max 5/second: Request 1 at 16 ms Request 2 at 237 ms Request 3 at 438 ms Request 4 at 648 ms Request 5 at 851 ms Request 6 at 1064 ms Request 7 at 1266 ms Request 8 at 1467 ms Request 9 at 1680 ms Request 10 at 1883 ms Total time: 1883 ms (expected ~1800ms for 10 calls at 5/sec) --- Batch API Processor --- Processing 8 items at max 3/second: Processing item_1 at 15 ms Processing item_2 at 354 ms Processing item_3 at 688 ms Processing item_4 at 1023 ms Processing item_5 at 1357 ms Processing item_6 at 1692 ms Processing item_7 at 2027 ms Processing item_8 at 2361 ms Batch complete in 2361 ms
This rate limiter is simple but effective for single-threaded scripts. For production multi-threaded applications, consider using Guava’s RateLimiter or a token bucket implementation.
Example 13: Real-World – Retry with Exponential Backoff
When a network call fails, you typically want to retry with increasing delays between attempts. This is called exponential backoff. It prevents overwhelming a struggling server with rapid retries. One implementation detail matters here: the retry loop is a plain for loop, not (1..maxRetries).each. A return inside an each closure only exits that single closure call, so with each the method would keep calling the action even after a success.
Example 13: Retry with Exponential Backoff
// Retry with exponential backoff
def retryWithBackoff(Map config = [:], Closure action) {
def maxRetries = config.maxRetries ?: 5
def initialDelay = config.initialDelay ?: 100
def maxDelay = config.maxDelay ?: 5000
def multiplier = config.multiplier ?: 2.0
def currentDelay = initialDelay
for (attempt in 1..maxRetries) {
try {
def result = action(attempt)
println " ✓ Attempt ${attempt}: Success!"
return result
} catch (Exception e) {
if (attempt == maxRetries) {
println " ✗ Attempt ${attempt}: Failed - ${e.message} (no more retries)"
throw e
}
// Add jitter (random variation) to prevent thundering herd
def jitter = (Math.random() * currentDelay * 0.1) as long
def sleepTime = Math.min(currentDelay + jitter, maxDelay)
printf " ↻ Attempt %d: Failed - %s (retrying in %dms)%n",
attempt, e.message, sleepTime
sleep(sleepTime)
currentDelay = Math.min((currentDelay * multiplier) as long, maxDelay)
}
}
}
// Simulate a flaky API that succeeds on the 4th try
def callCount = 0
println "--- Simulating Flaky API ---"
def startTime = System.currentTimeMillis()
try {
retryWithBackoff(maxRetries: 5, initialDelay: 100) { attempt ->
callCount++
if (callCount < 4) {
throw new RuntimeException("Connection refused")
}
return "Success data from API"
}
} catch (Exception e) {
println "Final failure: ${e.message}"
}
def elapsed = System.currentTimeMillis() - startTime
println "Total time: ${elapsed}ms, Total calls: ${callCount}"
// Simulate complete failure
println "\n--- Simulating Complete Failure ---"
def failStart = System.currentTimeMillis()
try {
retryWithBackoff(maxRetries: 3, initialDelay: 100, maxDelay: 500) { attempt ->
throw new RuntimeException("Server unavailable")
}
} catch (Exception e) {
println "Gave up after ${System.currentTimeMillis() - failStart}ms"
}
Output
--- Simulating Flaky API --- ↻ Attempt 1: Failed - Connection refused (retrying in 101ms) ↻ Attempt 2: Failed - Connection refused (retrying in 209ms) ↻ Attempt 3: Failed - Connection refused (retrying in 438ms) ✓ Attempt 4: Success! Total time: 930ms, Total calls: 4 --- Simulating Complete Failure --- ↻ Attempt 1: Failed - Server unavailable (retrying in 102ms) ↻ Attempt 2: Failed - Server unavailable (retrying in 215ms) ✗ Attempt 3: Failed - Server unavailable (no more retries) Gave up after 333ms
Exponential backoff with jitter is a standard pattern used by AWS, Google Cloud, and most production systems. The jitter prevents multiple clients from retrying at exactly the same time (called the “thundering herd” problem).
Example 14: Building a Timing Report
Let us combine everything into a full example that times a multi-step process and generates a report.
Example 14: Timing Report
class StopWatch {
private long startTime
private List<Map> laps = []
private String name
StopWatch(String name) {
this.name = name
this.startTime = System.nanoTime()
}
void lap(String label) {
def now = System.nanoTime()
def elapsed = (now - startTime) / 1_000_000.0
def lapTime = laps ? elapsed - laps.last().cumulative : elapsed
laps << [label: label, lapTime: lapTime, cumulative: elapsed]
}
void report() {
def totalTime = (System.nanoTime() - startTime) / 1_000_000.0
println "\n╔══════════════════════════════════════════════════╗"
printf "║ StopWatch: %-37s║%n", name
println "╠══════════════════════════════════════════════════╣"
printf "║ %-20s %8s %10s ║%n", "Step", "Lap (ms)", "Total (ms)"
println "╟──────────────────────────────────────────────────╢"
laps.each { lap ->
def pct = (lap.lapTime / totalTime * 100) as int
def bar = '█' * Math.max(1, (pct / 5) as int)
printf "║ %-20s %8.2f %10.2f ║ %s%n",
lap.label, lap.lapTime, lap.cumulative, bar
}
println "╟──────────────────────────────────────────────────╢"
printf "║ %-20s %8s %10.2f ║%n", "TOTAL", "", totalTime
println "╚══════════════════════════════════════════════════╝"
}
}
// Use the StopWatch for a multi-step data pipeline
def sw = new StopWatch("Data Pipeline")
// Step 1: Generate data
def data = (1..50_000).collect { [id: it, value: Math.random() * 1000] }
sw.lap("Generate data")
// Step 2: Filter
def filtered = data.findAll { it.value > 500 }
sw.lap("Filter (>500)")
// Step 3: Sort
def sorted = filtered.sort { it.value }
sw.lap("Sort by value")
// Step 4: Transform
def transformed = sorted.collect {
[id: it.id, value: it.value, category: it.value > 750 ? 'HIGH' : 'MEDIUM']
}
sw.lap("Transform")
// Step 5: Aggregate
def summary = transformed.groupBy { it.category }
.collectEntries { cat, items -> [cat, [count: items.size(), avg: items.average { it.value }]] }
sw.lap("Aggregate")
// Step 6: Format output
def report = summary.collect { cat, stats ->
sprintf(" %-8s: %5d items, avg: %.2f", cat, stats.count, stats.avg)
}.join("\n")
sw.lap("Format report")
// Print results
println "\nPipeline Results:"
println report
println "Records: ${data.size()} → ${filtered.size()} → ${transformed.size()}"
// Print timing report
sw.report()
Output
Pipeline Results: MEDIUM : 12379 items, avg: 626.23 HIGH : 12500 items, avg: 875.28 Records: 50000 → 24879 → 24879 ╔══════════════════════════════════════════════════╗ ║ StopWatch: Data Pipeline ║ ╠══════════════════════════════════════════════════╣ ║ Step Lap (ms) Total (ms) ║ ╟──────────────────────────────────────────────────╢ ║ Generate data 18.42 18.42 ║ ████████ ║ Filter (>500) 3.21 21.63 ║ █ ║ Sort by value 12.87 34.50 ║ ██████ ║ Transform 4.56 39.06 ║ ██ ║ Aggregate 2.34 41.40 ║ █ ║ Format report 0.89 42.29 ║ █ ╟──────────────────────────────────────────────────╢ ║ TOTAL 42.29 ║ ╚══════════════════════════════════════════════════╝
This StopWatch class is something you can copy into any Groovy project for quick profiling. It shows both individual step times and cumulative totals, with a visual bar chart for easy identification of bottlenecks.
Measuring Elapsed Time
To summarize the two approaches for measuring elapsed time:
System.currentTimeMillis()– Returns wall-clock time in milliseconds. Good for general timing (operations > 1ms). Can be affected by clock adjustments (NTP sync, daylight saving).System.nanoTime()– Returns a monotonic counter in nanoseconds. Best for benchmarking and precise timing. Not affected by clock adjustments. Cannot be used to determine current time of day.
Use nanoTime() for performance measurement and benchmarking. Use currentTimeMillis() when you need timestamps or when millisecond precision is sufficient.
Benchmarking Code Blocks
Effective benchmarking requires several practices that we demonstrated in Example 7:
- Warmup runs – The JVM’s JIT compiler optimizes code as it runs. The first few executions are slower. Always run 3-5 warmup iterations before measuring.
- Multiple iterations – Run at least 10-20 measured iterations to account for GC pauses and OS scheduling.
- Statistical analysis – Look at median and P95, not just average. The average can be skewed by outliers (GC pauses).
- Use nanoTime – For sub-millisecond operations,
currentTimeMillis()does not have enough resolution. - Prevent dead code elimination – Make sure you use the result of the computation, otherwise the JIT compiler might optimize it away entirely.
For serious JVM benchmarking, consider using JMH (Java Microbenchmark Harness). But for everyday Groovy scripting, the benchmark() function from Example 7 is more than sufficient.
Timeout Patterns
We covered the basic timeout pattern in Example 8. Here are the common timeout strategies:
- Poll with timeout (Example 8) – Check a condition repeatedly until it is true or time runs out
- Future with timeout – Submit work to an executor and call
future.get(timeout, TimeUnit.SECONDS) - Thread.join with timeout – Wait for a thread to finish with
thread.join(timeoutMs) - CompletableFuture.orTimeout – Java 9+ feature that automatically completes with a
TimeoutException
Always use timeouts when waiting for external resources (network, files, user input). A missing timeout is one of the most common causes of hung applications, and pairing every groovy sleep poll loop with a deadline is the cheapest insurance you can buy.
ScheduledExecutor and Timer
We covered both in Examples 9 and 10. Here is the quick comparison to help you choose:
| Feature | Timer | ScheduledExecutorService |
|---|---|---|
| Thread pool | Single thread | Configurable pool size |
| Exception handling | Task exception kills the timer | Task exception is contained |
| Task overlap | Cannot handle overlap | Handles concurrent tasks |
| Shutdown | cancel() | shutdown() + awaitTermination() |
| Best for | Simple scripts | Production applications |
Real-World Use Cases
We covered three major real-world patterns in the examples:
- Polling (Example 11) – Waiting for external conditions with regular checks and timeout safety
- Rate Limiting (Example 12) – Controlling the speed of API calls or batch processing
- Retry with Backoff (Example 13) – Handling transient failures with increasing delays between retries
These three patterns appear in almost every production system. The groovy sleep based implementations shown here are perfect for Groovy scripts and small applications. For large-scale production systems, consider using libraries like resilience4j or Spring Retry that add circuit breakers, metrics, and more sophisticated retry policies.
Best Practices
- Prefer Groovy’s
sleep()overThread.sleep()in Groovy scripts – cleaner syntax and built-in interrupt handling - Never use sleep for precise timing – it guarantees a minimum delay, not an exact one
- Always include timeouts when waiting for external resources – a missing timeout leads to hung processes
- Use
nanoTime()for benchmarking,currentTimeMillis()for general timing - Include warmup runs in benchmarks – JVM JIT compilation makes early runs slower
- Use exponential backoff with jitter for retries – prevents thundering herd problems
- Prefer
ScheduledExecutorServiceoverTimerfor production code – better exception handling and thread management - Always shut down executors – call
shutdown()andawaitTermination()to prevent resource leaks - Avoid busy-waiting – always sleep between poll iterations rather than spinning in a tight loop
Conclusion
Timing and sleeping are fundamental tools in any programmer’s toolkit. We covered 14 practical examples showing how to pause execution with sleep() and Thread.sleep(), measure performance with nanoTime() and currentTimeMillis(), benchmark code with warmup and statistical analysis, and implement real-world patterns like polling, rate limiting, and retry with backoff.
The standout feature of groovy sleep is the GDK method’s interrupt-handling closure. It is cleaner than Java’s Thread.sleep() and gives you a graceful way to handle thread interrupts without boilerplate try-catch blocks.
Next up in the series, we will cover Groovy closures – the flexible anonymous functions that make patterns like the ones we used today (timing blocks, retry handlers, poll conditions) possible.
Summary
- Groovy’s GDK
sleep(ms)is preferred overThread.sleep(ms)in Groovy scripts sleep(ms) { closure }handles interrupts gracefully with a closure- Use
System.nanoTime()for precise benchmarking,System.currentTimeMillis()for general timing - Always include timeouts when waiting for external conditions
- Use exponential backoff with jitter for retry logic
ScheduledExecutorServiceis the modern way to schedule recurring tasks
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Closures – The Complete Guide
Frequently Asked Questions
What is the difference between sleep() and Thread.sleep() in Groovy?
The groovy sleep() GDK method is available on every object and handles InterruptedException internally. Thread.sleep() is the standard Java method that throws InterruptedException (though Groovy does not force you to catch it). Groovy’s sleep() also accepts an optional closure for custom interrupt handling. Prefer sleep() in Groovy scripts for cleaner code.
How do I measure elapsed time in Groovy?
Use System.nanoTime() for precise benchmarking or System.currentTimeMillis() for general timing. Call it before and after the code you want to measure, then subtract: def start = System.nanoTime(); yourCode(); def elapsed = System.nanoTime() – start. Divide by 1_000_000 to convert nanoseconds to milliseconds.
How do I add a timeout to a Groovy operation?
Use a polling pattern: record the start time, loop with sleep intervals, and check both your condition and whether the elapsed time exceeds the timeout. Alternatively, use Future.get(timeout, TimeUnit) with an ExecutorService, or CompletableFuture.orTimeout() in Java 9+. Always include timeouts when waiting for external resources.
How do I implement retry with backoff in Groovy?
Create a loop with a maximum retry count. On each failure, sleep for an increasing delay (multiply by 2 for exponential backoff). Add random jitter to prevent multiple clients retrying simultaneously. Cap the maximum delay. Example: initialDelay=100ms, multiply by 2 each retry, max 5000ms, with 10% random jitter.
What is the difference between ScheduledExecutorService and Timer in Groovy?
ScheduledExecutorService is the modern Java concurrency approach – it supports multiple threads, contains task exceptions (one failed task does not kill others), and integrates with the executor framework. Timer uses a single thread, and an unhandled exception in a task kills the entire timer. Use ScheduledExecutorService for production code and Timer for simple scripts.
Interview Questions on Groovy Sleep and Timing
“Try each one aloud first; the phrasing you produce under mild pressure is what interviews measure.”
Q: A teammate aborts a Jenkins job, but a Groovy step sitting in sleep(60000) keeps running the full minute before dying. Why does the abort not cut it short?
Because the GDK sleep() deliberately eats interrupts. When the abort interrupts the thread, Groovy catches the InterruptedException and goes back to sleep for the remaining time, which is the exact opposite of what Thread.sleep() does. If you want the pause to be abortable, pass the interrupt closure and return true from it, as in sleep(60000) { true }, or use Thread.sleep() directly. The gotcha interviewers listen for: convenient default, but it makes long groovy sleep calls uninterruptible unless you opt out.
Q: A benchmark using System.currentTimeMillis() occasionally reports zero or even negative elapsed time. What is wrong with it?
Two things. Its resolution is roughly 10 to 15 milliseconds on some platforms, so anything faster rounds down to zero. Worse, it reads the wall clock, and the wall clock moves: an NTP sync or daylight-saving shift mid-measurement can make the end time earlier than the start, hence negative durations. System.nanoTime() fixes both, it is a monotonic counter designed for measuring intervals, immune to clock adjustments. The flip side to mention: nanoTime() values mean nothing as timestamps, only differences between two readings are valid.
Q: A Spock test does sleep 500 to wait for an async result. It passes locally but fails randomly on loaded CI agents. How do you fix it properly?
Fixed sleeps encode an assumption about machine speed, and CI agents violate it. The fix is polling with a deadline: loop, check the condition, sleep a short interval like 50 milliseconds, and give up only after a generous timeout. That way the test completes as soon as the condition holds, instead of always paying 500 milliseconds, and it tolerates slow agents up to the timeout. In Spock specifically, mention PollingConditions, which packages exactly this pattern. Saying “never assert after a bare sleep” is the answer interviewers want to hear.
Q: Your first timing run of a Groovy function shows 40 ms, but every following run shows 2 ms. Which number do you report?
Neither, until you understand them. The first run pays for class loading and interpretation; after enough iterations the JIT compiles the hot path, which is where the 2 ms comes from. So you do warmup iterations, then measure many runs and look at median and P95 rather than the average, because GC pauses skew means. Also make sure the result of the computation is actually used, or the JIT may eliminate the code entirely and you end up benchmarking nothing. For anything serious, reach for JMH instead of hand-rolled loops.
Q: A recurring cleanup task scheduled with Timer ran fine for weeks, then silently stopped. Nothing in the logs. What is your first suspicion?
An unhandled exception in one execution. Timer runs everything on a single thread, and one runtime exception kills that thread, taking every scheduled task with it, silently. My first move is wrapping the task body in try-catch with logging, then migrating to ScheduledExecutorService. Worth adding the subtlety: the executor is not magic either, an exception there quietly cancels that task’s future runs while the executor itself survives, so you still want the try-catch inside the task. Silent death of scheduled work is almost always an uncaught exception.
Related Posts
Previous in Series: Groovy print vs println – Output Methods
Next in Series: Groovy Closures – The Complete Guide
Related Topics You Might Like:
- Groovy File Read, Write, and Delete
- Groovy Hello World – Your First Program
- Groovy String Tutorial – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment