A 502 Bad Gateway is deceptive. The error shows up at the browser or API client, but the actual cause could be sitting anywhere: a crashed process, a misconfigured proxy, a failed deployment, or a dependency that never made it to the server.
This guide walks through a proven, layer-by-layer approach to diagnosing and fixing 502 errors on a FastAPI application managed by PM2, sitting behind Nginx and Cloudflare. Follow the steps in order. Each one either confirms the problem or eliminates a possible cause, so you’re never guessing.
Before You Start
This guide assumes your stack looks something like this:
- FastAPI running inside a Python virtual environment (
venv) - PM2 manages the process
- Nginx as the reverse proxy
- Cloudflare in front (optional, but common)
The commands below will work on any Linux server. Substitute your-service-name with whatever you named your PM2 process.
Step 1: Check the PM2 Process; and Read the Restart Count
pm2 list
The status column will likely say online. Don’t stop there. Look at the restart count.
If that number is in the hundreds or thousands, the process is not healthy — PM2 is catching a crash and immediately relaunching the application in a loop. A high restart count almost always means the application is failing at startup, which is exactly what causes a 502: the backend is never actually ready to accept connections.
Step 2: Confirm the Code Was Deployed Correctly
If you just pushed new code before the 502 appeared, verify that the new routes or functions actually exist on the server:
grep -R "your-route-name" /path/to/your/app
If the expected code is there, the deployment itself isn’t the problem. Move on. If it’s missing, your git pull likely failed silently or targeted the wrong branch, fix that first before continuing.
Step 3: Read the Application Logs
This is where most 502 investigations end. Run:
pm2 logs your-service-name --lines 100
Scan for Python exceptions, particularly anything near the top of the startup sequence. The error you’re looking for often looks like this:
ModuleNotFoundError: No module named 'your-package-name'
If you see something like that, the application is crashing before it can bind to a port. Nginx (or Cloudflare) has nowhere to forward the request; hence the 502. The fix is in the next steps.
If the logs show something else entirely- a syntax error, a configuration issue, a failed database connection, follow that thread instead.
Step 4: Verify the Missing Dependency
Don’t assume the log is right. Confirm it directly inside the virtual environment:
pip show package-name
Then do a live import test:
python -c "from package_name import SomeClass"
If both fail with an error, the package is genuinely absent from the environment. If pip show Finds it, but the import still fails; you may have a version conflict or a broken install; try reinstalling it explicitly.
Step 5: Check Whether It Was Ever in requirements.txt
grep -i package-name requirements.txt
If it’s not there, you have two problems: the missing package caused today’s outage, and the incomplete requirements.txt is what will cause the next one. Both need fixing.
Step 6: Add the Package to requirements.txt
Open requirements.txt and add the package with a minimum version constraint:
your-package>=x.x.x
Commit the change and pull it to the server:
git pull origin main
This makes the dependency part of the project’s source of truth, so anyone who sets up the environment fresh will get it automatically.
Step 7: Install the Package in the Virtual Environment
pip install package-name
Once installed, run the import test again to confirm it resolves cleanly:
python -c "from package_name import SomeClass; print('OK')"
You should see OK printed. If you see another error, the package may have its own missing dependencies — run pip install again and read the output carefully for any warnings.
Step 8: Restart the Service
pm2 restart your-service-name
Watch the process. The restart count should stop climbing, and memory usage should stabilise. If it’s still crashing, go back to Step 3 and look for a second error that the first one was masking.
Step 9: Test the Endpoint
curl -X POST https://your-domain.com/your-api-path/your-endpoint -i
What you’re looking for here is any response other than 502. A 422 Unprocessable Entity means the request passed through Cloudflare, through Nginx, reached FastAPI, and entered your route handler — it just needs a valid request body. That’s a complete resolution of the 502.
A 404 means routing is off. A 500 means there’s an application error inside the handler. Both of those are normal application-level problems, not infrastructure ones.
Step 10: Update Your Deployment Script
This step is what separates a one-time fix from a permanent one.
If your current deployment looks like this:
cd /path/to/your/app
git pull origin main
pm2 restart your-service-name
It will work fine until someone adds a new dependency. Then it will silently deploy broken code, and you’ll be back here.
Change it to this:
cd /path/to/your/app
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
pm2 restart your-service-name
pip install -r requirements.txt is effectively a no-op when nothing has changed — it completes in seconds. But when a new package has been added, it catches it automatically without anyone having to remember to run it manually.
Quick Reference: What Each Step Rules Out
| Step | What You’re Checking | What a Pass Confirms |
|---|---|---|
| 1 | PM2 status + restart count | Whether the process is stable |
| 2 | Route definitions in deployed code | Deployment completed successfully |
| 3 | PM2 application logs | Root cause of startup failure |
| 4 | pip + direct import test | The package is genuinely missing |
| 5 | requirements.txt contents | Whether it was ever declared |
| 6–7 | Add, commit, and install the package | Import error resolved |
| 8 | PM2 restart + stability | Application starts cleanly |
| 9 | curl endpoint test | 502 resolved end-to-end |
| 10 | Deployment script | Prevention for future releases |
Why This Happens
FastAPI (like any Python application) imports all its dependencies at startup. If a single import fails, the entire process exits. PM2 restarts it, the import fails again, and the cycle continues indefinitely. Meanwhile, Nginx keeps receiving requests it has nowhere to forward, and returns a 502 to every caller.
The root trigger is usually a new feature that uses a package the developer had installed locally, but never added to requirements.txt. The package works in development, gets committed, and the deployment pulls the code — but not the dependency.
Running pip install -r requirements.txt on every deploy is the single most reliable safeguard against this class of failure.











