How to Deadlock: A Practical Step-by-Step Guide for Testing and Debugging

If you’ve ever spent three hours debugging a stuck e-commerce checkout flow that freezes the second 12 users try to apply the same limited-time discount code at once, you’ve almost certainly encountered a deadlock. Most developers spend their entire careers trying to avoid these system halts, but if you want to build robust detection tools, test recovery workflows, or simply understand how these errors work at a core level, you need to learn how to deadlock systems intentionally and safely. This guide walks through practical, low-risk methods to create deadlocks across operating systems, databases, and custom apps, plus guardrails to make sure your testing doesn’t take down production systems by accident. If you’re a student just learning about concurrency, you might be assigned an exercise that asks you how to deadlock a simple multi-threaded program, and the steps we cover here will work for that use case too.

How to Deadlock Safely: Core Pre-Requisites Before You Start

Before you run any deadlock tests, you have to prioritize safety first. Even a small, intentional deadlock can crash shared resources, block team members’ work, or cause unexpected outages if you run it in the wrong environment. You don’t need fancy tools to run these tests, but you do need a few key guardrails in place before you begin.

  • An isolated virtual machine or local dev environment with no connection to shared production resources
  • Admin access to the system or database you’re testing, so you can terminate stuck processes if needed
  • A pre-written kill script or command to end deadlocked processes immediately if your test gets out of hand
  • Clear documentation of what you’re testing, so you can replicate results later for debugging

If you’re running tests on a work-owned system, always get approval from your DevOps team first, even if you’re 100% sure the environment is isolated. Staging environments are often shared across dozens of team members, and a deadlock on a shared staging database can block deployments or testing work for hours. Never run deadlock tests on any system that handles real user data or live traffic, even if you think the test is low-risk. It only takes one small mistake to cause a user-facing outage that could cost your company thousands of dollars in lost revenue.

Understanding the 4 Necessary Deadlock Conditions

You can’t create a deadlock by accident, and you can’t create one on purpose unless all four of the following core conditions are met. If even one of these four conditions is missing, you won’t be able to figure out how to deadlock the system, so you need to make sure all four are in place for your test. These conditions apply to every type of system, from local desktop apps to distributed cloud databases.

The first condition is Mutual exclusion, which means at least one resource is held in a non-shareable mode, so only one process can use it at a time. For example, a write lock on a database row can only be held by one transaction at once, so no other transaction can modify that row until the lock is released. The second condition is Hold and wait, which means a process is holding at least one resource and requesting additional resources that are currently held by other processes. For example, a thread holding Lock A might request Lock B, which is already held by a different thread.

The third condition is No preemption, which means resources can’t be forcibly taken from a process that’s holding them; the process has to release them voluntarily. Most operating systems and databases follow this rule for standard locks, to avoid corrupting data mid-transaction. The fourth condition is Circular wait, which means a set of processes form a circular chain where each process holds a resource that the next process in the chain is requesting. The simplest example is two processes: Process A holds Lock 1 and waits for Lock 2, while Process B holds Lock 2 and waits for Lock 1.

Practical Examples of Deadlock Creation Across Common Systems

Once you understand the four conditions, you can figure out how to deadlock almost any system that uses shared locks. The examples below are simple to replicate, low-risk, and work for most standard testing or learning use cases. You can adjust them to fit your specific tech stack as needed.

For an operating system-level deadlock, you can use a simple multi-threaded script in a language like Python or Java. Write two threads that each need to acquire two separate locks to complete their work. Have Thread 1 acquire Lock A first, then wait 100 milliseconds, then try to acquire Lock B. Have Thread 2 acquire Lock B first, wait 100 milliseconds, then try to acquire Lock A. Run the script, and within a few seconds you’ll hit a deadlock: Thread 1 will hold Lock A and wait forever for Lock B, while Thread 2 holds Lock B and waits forever for Lock A. You can confirm the deadlock by checking your system’s process monitor, which will show both threads using 0% CPU and stuck in a waiting state.

For a database deadlock, open two separate SQL sessions connected to a local testing database. In Session 1, run an UPDATE query on row 1 of a test table (for example, UPDATE users SET name = 'Test' WHERE id = 1) and do not commit the transaction. This places a write lock on row 1. In Session 2, run an UPDATE query on row 2 of the same table and do not commit, placing a write lock on row 2. Go back to Session 1 and run an UPDATE query on row 2, which will hang waiting for Session 2’s lock to release. Then go to Session 2 and run an UPDATE query on row 1, which will hang waiting for Session 1’s lock. You now have a full database deadlock. Most modern databases will detect this automatically and terminate one of the transactions after a set timeout, so you’ll usually see a deadlock error message within 30 seconds or less.

For a distributed system deadlock, you can use two simple local microservices. Build Service A to call Service B and wait for a response before returning data, and build Service B to call Service A and wait for a response before returning data. Trigger a call to Service A and Service B at the exact same time, and you’ll create a deadlock: Service A will wait forever for Service B’s response, and Service B will wait forever for Service A’s response. Distributed deadlocks are much harder to detect than local or database deadlocks, because there’s no central lock manager tracking all requests across services.

What to Do After You Create a Deadlock for Testing

Creating a deadlock is only the first step of the testing process. What you do after the deadlock forms will determine how much value you get from the test, and whether you leave your testing environment in a usable state for future work.

First, if you’re testing deadlock detection tools, check if your monitoring system picks up the deadlock as expected. Note how long it takes for the alert to fire, if it correctly identifies which processes or transactions are stuck, and if it provides enough context for an engineer to resolve the issue quickly. If your system has automated recovery workflows, test if they work as intended: does the system terminate the lowest-priority process, roll back the correct transaction, or release locked resources without crashing the entire system? You don’t need to learn how to deadlock every type of system you work with, but testing the most common deadlock scenarios for your stack will pay off massively when a real issue hits.

Next, document every part of the test: what conditions you set up, how long it took for the deadlock to occur, what detection and recovery steps worked, and what gaps you found. For example, if your database monitoring didn’t alert you to the deadlock for 10 minutes, that’s a critical gap you need to fix before a deadlock hits production. If your automated recovery tool terminated the wrong transaction, you need to adjust its priority rules to avoid costly data loss in the future.

Finally, clean up your testing environment completely after you’re done. Even if the system resolved the deadlock automatically, check for leftover locks, hanging processes, or uncommitted transactions that could slow down the environment or cause unexpected issues for other users. If you’re using a virtual machine for testing, you can simply roll it back to a pre-test snapshot to make sure all leftover resources are cleared out.

Learning how to deadlock systems intentionally is a skill that will make you a much stronger developer, DevOps engineer, or database administrator. It’s far easier to build reliable detection and recovery systems when you’ve seen firsthand how deadlocks form, what they look like in monitoring tools, and how they impact end users. Just remember to always work in an isolated environment, follow the safety guardrails we outlined, and never run tests on live production systems. With a little practice, you’ll be able to replicate almost any deadlock scenario you encounter, fix underlying issues faster, and build systems that are far more resilient to unexpected halts.