Groovy Sleep and Timing – Pause Execution with 14 Practical Examples

Learn Groovy sleep(), Thread.sleep(), timing with nanoTime, benchmarking, and timeout patterns with 14 practical examples. Tested on Groovy 5.x.

“Timing is everything – in comedy, in cooking, and especially in programming.”

Rob Pike, Notes on Programming in C

Last Updated: March 2026 | Tested on: Groovy 5.x, 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.

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.

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.

MethodSourceInterrupt HandlingParameter
Thread.sleep(ms)Java SDKThrows InterruptedExceptionMilliseconds (long)
sleep(ms)Groovy GDKSwallows interrupt, returns booleanMilliseconds (long)
sleep(ms) { closure }Groovy GDKPasses exception to closureMilliseconds + 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.

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
def sum = 0
(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: 45 ms

--- Operation Timings ---
  String concatenation     :    12 ms
  StringBuilder            :     1 ms
  List creation            :     8 ms
  Map creation             :     5 ms

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: 2450000 nanoseconds
Elapsed: 2450.00 microseconds
Elapsed: 2.450 milliseconds

--- Precision Comparison ---
Using currentTimeMillis:
  Run 1: 3 ms
  Run 2: 1 ms
  Run 3: 0 ms
  Run 4: 1 ms
  Run 5: 0 ms

Using nanoTime:
  Run 1: 2.150 ms
  Run 2: 0.890 ms
  Run 3: 0.450 ms
  Run 4: 0.380 ms
  Run 5: 0.350 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()
}
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        :   18.500 ms
⏱ Sort the list                 :   35.200 ms
⏱ Find max value                :    5.100 ms
⏱ Filter even numbers           :   12.300 ms
⏱ Sum all values                :    8.700 ms
⏱ Convert to set                :   42.600 ms

--- With Return Values ---
⏱ Calculate sum                 :   28.400 ms
Result: 500000500000
⏱ Sort 10,000 random numbers    :   15.800 ms
First 5: [2, 5, 8, 12, 15], Last 5: [99985, 99990, 99993, 99997, 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:   21.828 ms
   Median:     6.552 ms
   Min:        1.951 ms
   Max:      164.583 ms
   P95:      164.583 ms

📊 StringBuilder (1000 appends) (20 runs)
   Average:    2.375 ms
   Median:     1.691 ms
   Min:        0.550 ms
   Max:       15.682 ms
   P95:       15.682 ms

📊 StringBuffer (1000 appends) (20 runs)
   Average:    1.940 ms
   Median:     1.580 ms
   Min:        0.490 ms
   Max:       12.340 ms
   P95:       12.340 ms

📊 ArrayList sort (10,000 elements) (20 runs)
   Average:    8.450 ms
   Median:     7.890 ms
   Min:        6.200 ms
   Max:       18.600 ms
   P95:       18.600 ms

📊 Array sort (10,000 elements) (20 runs)
   Average:    2.180 ms
   Median:     1.950 ms
   Min:        1.500 ms
   Max:        5.200 ms
   P95:        5.200 ms

This benchmark clearly shows that StringBuilder is about 35x faster than string concatenation for 1000 appends. 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    0 ms
  Request  2 at  200 ms
  Request  3 at  400 ms
  Request  4 at  600 ms
  Request  5 at  800 ms
  Request  6 at 1000 ms
  Request  7 at 1200 ms
  Request  8 at 1400 ms
  Request  9 at 1600 ms
  Request 10 at 1800 ms

Total time: 1800 ms (expected ~1800ms for 10 calls at 5/sec)

--- Batch API Processor ---
Processing 8 items at max 3/second:
  Processing item_1     at     0 ms
  Processing item_2     at   333 ms
  Processing item_3     at   666 ms
  Processing item_4     at  1000 ms
  Processing item_5     at  1333 ms
  Processing item_6     at  1666 ms
  Processing item_7     at  2000 ms
  Processing item_8     at  2333 ms
Batch complete in 2333 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.

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

    (1..maxRetries).each { attempt ->
        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 105ms)
  ↻ Attempt 2: Failed - Connection refused (retrying in 215ms)
  ↻ Attempt 3: Failed - Connection refused (retrying in 420ms)
  ✓ Attempt 4: Success!
Total time: 745ms, Total calls: 4

--- Simulating Complete Failure ---
  ↻ Attempt 1: Failed - Server unavailable (retrying in 108ms)
  ↻ Attempt 2: Failed - Server unavailable (retrying in 210ms)
  ✗ Attempt 3: Failed - Server unavailable (no more retries)
Gave up after 325ms

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.

ScheduledExecutor and Timer

We covered both in Examples 9 and 10. Here is the quick comparison to help you choose:

FeatureTimerScheduledExecutorService
Thread poolSingle threadConfigurable pool size
Exception handlingTask exception kills the timerTask exception is contained
Task overlapCannot handle overlapHandles concurrent tasks
Shutdowncancel()shutdown() + awaitTermination()
Best forSimple scriptsProduction 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 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() over Thread.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 ScheduledExecutorService over Timer for production code – better exception handling and thread management
  • Always shut down executors – call shutdown() and awaitTermination() 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 is Groovy’s GDK sleep() method with its 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 over Thread.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
  • ScheduledExecutorService is 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?

Groovy’s sleep() is a GDK method available on every object that 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.

Previous in Series: Groovy print vs println – Output Methods

Next in Series: Groovy Closures – The Complete Guide

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *