1. Overview

In this tutorial, we’ll illustrate how to schedule an asynchronous task in a route handler while using the Play Framework.

First, we’ll specify when we can use asynchronous tasks. Then, we’ll take a look at two methods of scheduling tasks in the Play Framework.

2. Why Do We Need Async Tasks?

There are at least three reasons to use asynchronous tasks in a Play Framework application:

  • starting a long-running process that involves multiple applications
  • triggering an update of the internal state of the application
  • tracking user actions without increasing the response time

Occasionally, we want to start a long-running task after receiving a call to the REST API. Let’s say we have a task that involves retrieving data from a database, generating a PDF report, and sending it to the user. It wouldn’t make much sense for the client application that uses our REST API to wait until the email is sent. It’s much better to send back the Accepted HTTP status right away and process the background task.

We may also want to clear the application cache or reload data and prepare the cache to serve responses. In this case, we don’t want to wait until the cache gets refreshed. After all, the HTTP connection will most likely timeout before the application finishes the process.

Finally, we may need to send events to track user actions to a message queue. If the tracking exists for monitoring purposes and the notification is not a crucial part of the business process, we may choose to send them in a background task.

3. Scheduling Functions as Async Tasks

The Play Framework 3.0 uses the Apache Pekko actor system as the underlying technology. However, Play 2.x uses Akka actors under the hood.

In our controller code, we can access the actor system and its scheduler. When we have access to the Scheduler, we can run any function as a background task.

First, we’ll add the ActorSystem as the controller dependency and ExecutionContext as the implicit dependency:

class AsyncTaskController @Inject()(val controllerComponents: ControllerComponents, val actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends BaseController {
  def runAsync(): Action[AnyContent] = Action {

To access the endpoint, we’ll add its configuration to the routes file:

GET /async controllers.AsyncTaskController.runAsync()

Now, we can get the ActorSystem‘s scheduler and use it to schedule an anonymous function to run with 30 seconds delay:

import scala.concurrent.duration._
import scala.language.postfixOps

def runAsync(): Action[AnyContent] = Action {
  Console.println(s"In route handler: ${DateTime.now()}")
  actorSystem.scheduler.scheduleOnce(30 seconds) {
    Console.println(s"30 seconds later: ${DateTime.now()}")

Note that the 30 seconds is a valid FiniteDuration object because we’ve imported the implicit conversions from the scala.concurrent.duration._ package and enabled postfix notation using import scala.language.postfixOps.

To verify that the anonymous function runs in the background and is delayed by at least 30 seconds, we’ll run the Play Framework application and call the endpoint by accessing http://localhost:9000/async in a browser. In the log, we’ll see two messages:

In route handler: 2021-02-10T18:09:22.639+01:00
30 seconds later: 2021-02-10T18:09:52.794+01:00

4. Using Actors

Using the ActorSystem has two main advantages:

  • actors can be stateful, yet at the same time, we don’t need to worry about concurrent access because actors process one message at a time
  • an actor can send back a response

To use actors in the Play Framework, we’ll need to define a Guice module that adds the actor to the actor system. Additionally, we’ll need to inject an actor reference to the controller. Let’s start with injecting the actor reference and sending a message to the actor:

class AsyncTaskController @Inject()(..., @Named("async-job-actor") actor: ActorRef)(implicit ec: ExecutionContext) extends BaseController {
  def runAsync(): Action[AnyContent] = Action {

The @Named(“async-job-actor”) annotation instructs the Play Framework which actor should be passed to the controller. Inside the runAsync function, we call the! the method, which sends the given object to the actor. We can pass any serializable object as the message.

To make it work, we need a few more things. First, we’ll define the actor that receives the message:

class AsyncTaskInActor extends Actor {
  override def receive: Receive = {
    case msg: String =>
      Console.println(s"Message ${msg} received at ${DateTime.now()}")

Next, we’ll add the binding in the Guice dependency injection module:

class ActorsModule extends AbstractModule with PekkoGuiceSupport {
  override def configure(): Unit = {
    bindActor(classOf[AsyncTaskInActor], "async-job-actor")

Finally, we’ll open the application.conf file and add the new module to the modules used by the Play Framework:

play.modules.enabled += "actors.ActorsModule"

4.1. Scheduling Messages Sent to Actors

If we want to delay a message sent to an actor, we can merge both solutions shown in this tutorial and use the scheduleOnce function that accepts an actor reference as the parameter:

  30 seconds,

4.2. Scheduling a Recurring Task

Occasionally, we may need to execute a recurring task. In that case, we use the scheduleAtFixedRate function, which accepts the initial delay of the task, the intervals between subsequent runs, an actor handler, and the message:

val cancellable = actorSystem.scheduler.scheduleAtFixedRate(
  10 seconds,
  5 minutes,
  "recurring task message"

As a return value, we get an instance of Cancellable, which we can use to stop the recurring task:


5. Conclusion

In this tutorial, we’ve demonstrated how to run anonymous functions asynchronously in the Play Framework by using the Scheduler directly and how to access the ActorSystem to run async tasks. We’ve shown how to define a Guice module that adds custom actors to the ActorSystem and injects actor references.

You can find the complete example over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.