Scala's Future.successful: Do Not Block Your Future Success

I want to raise awareness of a simple mistake that can be made with the successful method of a Scala Future. A Future is a placeholder object for a value that may not yet exist. It is used for performing operations concurrently, in a non-blocking manner, and it enables scaling workloads efficiently across shared resources. The Future.successful method constructs an already completed Future with the specified result. The fact that the Future can be completed immediately is more efficient.

To demonstrate, consider the following trivial example that uses the Akka Streams API to concurrently execute up to eight Futures. Every Future is completed immediately with the literal value "abc123". This example takes approximately one second to execute on my computer. Interestingly, it only consumes one CPU core, despite the mapAsync parallelism being set to eight (more on this later).

Source(1L to 10000000L)  
  .mapAsync(8)(_ => Future.successful("abc123"))
  .runWith(Sink.ignore)

If I modify this example to use the Future.apply method, instead of Future.successful, it takes over 20 seconds to execute, even though it uses all eight CPU cores, 20 times slower than using Future.successful!

Source(1L to 10000000L)  
  .mapAsync(8)(_ => Future("abc123"))
  .runWith(Sink.ignore)

Why is this?

The Future.apply method creates a Future and schedules it to run on the execution context. It may run on the current thread, but most likely it will run on another thread. This comes at the cost of additional memory allocations, context switches, and synchronization among threads. An individual result may be fractionally slower when executed asynchronously, rather than synchronously. It is important to recognize that asynchronous programming is about improving the performance and scalability of a service in aggregate—maximizing the use of shared computing resources in the process—not necessarily the most efficient execution of a specific, individual task.

Armed with the knowledge that Future.successful is more efficient, people try and use it judiciously. I think the name is also enticing, encouraging people to wrap the happy code-path in Future.successful, mirroring the use of Future.failed in error handling logic. However, do not overlook this very important fact:

The code executed inside the Future.successful method will block the current thread.

This is almost certainly not what you intend, but it is subtle, and an easy mistake to make. It is also easy to overlook in a code review.

To demonstrate, the following example simulates a CPU-intensive workload by consuming the CPU in a while loop for approximately 10 milliseconds.

Source(1L to 10000L)  
  .mapAsync(8) { value =>
    Future {
      val start = System.currentTimeMillis()
      while ((System.currentTimeMillis() - start) < 10) {}
      value
    }
  }
  .runWith(Sink.ignore)

This example completes in just over 10 seconds on my computer, consuming all eight cores in the process. But notice what happens when the Future.apply is changed to Future.successful. The following example takes over over 100 seconds to execute, an order of magnitude longer. It only consumes a single core, essentially running synchronously.

Source(1L to 10000L)  
  .mapAsync(8) { value =>
    Future.successful {
      val start = System.currentTimeMillis()
      while ((System.currentTimeMillis() - start) < 10) {}
      value
    }
  }
  .runWith(Sink.ignore)

The code inside the Future.successful method is executed on the current thread and it blocks the current thread until it is complete, essentially defeating the purpose of executing code in a Future, in an asynchronous and non-blocking manner.

Imagine the potential impact of inadvertently capturing a network request, or a database insert, in a Future.successful. Even comparatively fast operations, like finding an element in a large data structure, or rendering JSON, can impact the overall performance and scalability of an application when inadvertently performed in a Future.successful.

When using Future.successful, ensure that the argument has already been computed. It is most appropriate to use Future.successful with a literal value—for example, Future.successful(42)—or a val, that already contains the result of an earlier computation. I am not aware of a static analysis tool that will highlight this situation. Although imperfect, I think it could be valuable to generate a warning if Future.successful is being called with an argument that is not a literal or a val.

When implementing asynchronous and concurrent programs, it is often difficult to validate our intentions regarding the execution, especially since the execution may not be deterministic. However, whenever possible, it can be really valuable to take the extra time to validate our assumptions, perhaps through constructing small experiments, like the ones I presented above. In this case, it was easy to see that Future.successful violated my assumption that eight operations would be executed concurrently, since it was executed on a single core, and it executed no faster than running the code synchronously, in a simple for loop.