How Blocking Calls in Futures Caused a Deadlock in Production



This content originally appeared on Level Up Coding – Medium and was authored by Anas Anjaria

Programming

A real-world story of thread pool starvation, why Await.result is dangerous, and how non-blocking Futures saved the day.

We had a tricky production issue: one of our apps suddenly froze and required manual intervention to recover. It was rare — but painful when it happened.

At first glance, the stacktrace pointed to MySQL. But after digging deeper with VisualVM and some careful code analysis, my colleague made the right call:

👉 The culprit wasn’t MySQL at all.
👉 It was thread starvation in our async code, which led to a deadlock.

Here’s how it happened — with reproducible examples.

TL;DR

Deadlocks in async code aren’t always about the database.

If you block a Future (Await.result), you risk thread pool starvation. When all threads are blocked waiting, no threads are left to complete work — creating a deadlock.

The fix: avoid blocking calls in Futures → use non-blocking composition (map, flatMap) instead.

Prerequisite: Futures and Thread Pools

How async tasks flow through the executor service: tasks enter the queue, wait for a free thread, and get executed by the thread pool — Created by author

This article assumes basic knowledge of Futures in Scala & ExecutionContext. Quick recap:

  • When you create a Future, it goes into a task queue.
  • If a thread is free → it runs immediately.
  • If no thread is free → it waits until one is available.

Random Deadlocks in Production

In production, we noticed rare but serious deadlocks: the app would stop working and needed manual intervention.

Logs showed MySQL involvement, but the real issue was in our async handling:

  • Blocking calls inside Future → Await.result(…, Duration.Inf) was used in multiple places.
  • Thread pool exhaustion → With a fixed-size pool, once threads were blocked, no threads remained to complete pending work.

Result: a deadlock where everything was waiting, but nothing was progressing.

🔍 Let’s Reproduce the Problem

Let’s break this down with a simple setup:

  • 1 thread in the pool
  • 2 async tasks

When Blocking Calls Cause Deadlock

// Future { ... } ---> Task-1
// asyncOp() -----> Task-2
val result = Future { Await.result(asyncOp(), 1.second) * 2 }

What happens?

  1. Future {. . .}, say Task-1, starts on the only available thread.
  2. Inside it, asyncOp() i.e Task-2tries to start another Future.
  3. But there are no free threads.
  4. Task-1 is waiting for Task-2, and Task-2 is waiting for a free thread.

→ Deadlock.

This unit test demonstrates it clearly

class ThreadPoolStarvationSpec extends AnyWordSpec with Matchers with BeforeAndAfterAll {
private val executorService = Executors.newFixedThreadPool(1)
private implicit val executionContext: ExecutionContext = ExecutionContext.fromExecutor(executorService)

private def asyncOp(): Future[Int] = Future {
Thread.sleep(100)
42
}

...

"ThreadPoolStarvation" should {
"throw timeout exception due to thread pool starvation" in {
// task occupy the only thread in the pool and block, waiting for asyncOperation to complete.
// But asyncOperation also needs a thread from the same pool to run!
// Since all threads in a pool, which in our case is only 1, are blocked,
// asyncOperation never runs, and the program deadlocks.
val task = Future { Await.result(asyncOp(), 1.second) * 2 }
intercept[Exception] {
Await.result(task, 2.seconds)
}
}
...
}
}

When Non-Blocking Calls Solve the Problem

Now replace the blocking call with non-blocking composition

val task = asyncOp().map(_ * 2)
val result = Await.result(task, 2.seconds)

This time:

  • Task-1 triggers asyncOp() (non-blocking).
  • The .map is queued to run when results are ready.
  • No thread is blocked.
  • Tasks complete smoothly.

→ No deadlock.

Github: https://github.com/anasanjaria/blogs/tree/main/scala-execution-context-deep-dive

⚠ What Could Go Worse?

In our production code, the issue was even nastier: blocking calls inside loops.
That meant not just one deadlock, but the potential to starve every thread repeatedly.

Lesson learned the hard way: never block inside Futures.

📘 What You’ve Learned

  • Blocking in Futures is dangerous → Await.result can starve your thread pool.
  • Deadlocks aren’t always DB-related → they can be caused by concurrency issues in your code.
  • Non-blocking composition (map, flatMap) is the safer path.

Conclusion

Not every “deadlock” is a database problem. Sometimes the database just takes the blame in your logs.

The real danger? Thread pool starvation from blocking calls.
Once we removed Await.result from our async code and replaced it with proper non-blocking patterns, the issue disappeared.

👉 If you’re using Futures, remember: don’t block, compose.

📚 Related Reading

Scala’s Execution Contexts: A Deep Dive

Fixing HikariCP SQLTransientConnectionException

📘I write actionable, experience-based articles on backend development — no fluff.

🔗 Find more of my work, connect on LinkedIn, or explore upcoming content: all-in-one


How Blocking Calls in Futures Caused a Deadlock in Production was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding – Medium and was authored by Anas Anjaria