Flask on Windows Service Pitfalls: 4 NSSM Fails, schtasks Solved It in 5 Minutes
On Sept 7 I spent an entire afternoon trying 4 different approaches: subprocess.POPEN / NSSM / NSSM+bat / NSSM+full Python path. The first 3 failed, the 4th made me sick of NSSM. In the last 5 minutes, I gave up on NSSM and switched to Windows-native
schtasks.exe + watchdog bat loop — first try worked.This article documents the 4 fails + 1 success complete process, with copy-paste commands, to save you 4 hours of pain.
1. Problem: Flask Dies Every Few Days
My server runs two Flask apps:
- PHD: HTTPS 8443 (ZeroSSL cert) + HTTP 8080, the BaccAI main AI prediction tool
- Old PRO: HTTPS 8000 + HTTP 80 (IIS owns port 80, 80-to-HTTPS redirect dropped), v3 algorithm
Both start with flask run or python app.py, seems normal. But in reality: PHD dies every 2-4 days, old PRO lasts a bit longer but also dies.
Death reasons are a mixed bag:
- Windows Update auto-reboot
- Antivirus cleaning "long-idle" processes
- Some edge case triggers Python exception exit (I didn't bother chasing)
- Power management misjudging the server as "idle"
Worst part: no alert on death. By the time users email me "site is down", hours have passed.
So on Sept 7 I decided to spend an afternoon fixing this: get Flask behaving like a Windows service, auto-restart on death, auto-launch on server reboot.
2. First Attempt: subprocess.Popen + DETACHED_PROCESS (Failed)
My initial idea was "have Flask detach itself on startup", code like this:
import subprocess
import sys
if '--detached' not in sys.argv:
subprocess.Popen(
[sys.executable, __file__, '--detached'],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
close_fds=True,
)
sys.exit(0)
# else start Flask
app.run(host='0.0.0.0', port=8443, ssl_context=('cert.pem', 'key.pem'))
Logic looks right: first launch, self-spawn a detached copy, then exit; copy starts Flask, decoupled from terminal.
Reality: when parent exits, Windows cleans up the whole process tree. Microsoft docs say "DETACHED_PROCESS won't auto-close console", but in practice, when I logged out via RDP, the Python child died with it.
Worse: no auto-restart on death. DETACHED_PROCESS just detaches, no supervisor process watching.
This approach hit the trash in 5 minutes.
3. Second Attempt: NSSM (Stuck for 1 Hour)
NSSM (Non-Sucking Service Manager) is the most popular tool on Windows for "wrapping any exe as a service".
Principle: NSSM installs a Windows service, on service start NSSM launches the exe you configured, when exe dies NSSM auto-restarts it.
Sounds perfect. I downloaded nssm.exe, dropped it in C:\Windows\System32\, and ran:
cmd /c "nssm install BaccAI_PHD D:\phd824\app.py"
cmd /c "nssm set BaccAI_PHD AppDirectory D:\phd824"
cmd /c "nssm set BaccAI_PHD AppStdout D:\phd824\app.log"
cmd /c "nssm set BaccAI_PHD AppStderr D:\phd824\app_err.log"
cmd /c "nssm set BaccAI_PHD AppRestartDelay 5000"
cmd /c "nssm set BaccAI_PHD AppExit Default Restart"
cmd /c "nssm start BaccAI_PHD"
First nssm start returned:
BaccAI_PHD: START: The system cannot find the file specified.
I checked everything: D:\phd824\app.py exists, directory accessible, Python 3.8.10 at C:\Users\Administrator\AppData\Local\Programs\Python\Python38\python.exe also exists.
Event Log showed:
EventID: 7000
The BaccAI_PHD service failed to start due to the following error: %%2
%%2 = ERROR_FILE_NOT_FOUND.
My troubleshooting steps:
- Python path has spaces, need quotes → added, still same error
- Python is Windows Store installed, service runs in LocalSystem context may lack permission → switched to Administrator account, failed
- AppParameters format wrong → set to AppParameters="D:\phd824\app.py", failed
- NSSM version too old → downloaded latest 2.24, failed
- Try cmd.exe as Application + /c calling Python → failed
2 hours in, 4 variants all fail. I started suspecting NSSM itself was broken on this machine.
Final "whiteboard test":
# Use NSSM to launch the simplest echo to see
cmd /c "nssm install BaccAI_TEST C:\Windows\System32\cmd.exe"
cmd /c 'nssm set BaccAI_TEST AppParameters /c "echo HELLO > D:\test.log"'
cmd /c "nssm start BaccAI_TEST"
Result: same "file not found" error.
This confirmed NSSM was completely broken on this machine — couldn't even launch cmd.exe, not a Python issue at all.
3 hours spent. Decision: abandon NSSM.
4. Third Attempt: NSSM + bat Wrapper (Failed)
Last gasp, I thought maybe it was the .bat file itself. Wrap it in a bat:
@echo off
cd /d D:\phd824
C:\Users\Administrator\AppData\Local\Programs\Python\Python38\python.exe app.py
Then NSSM Application pointed to D:\phd824\start_phd.bat.
Result: same "file not found" error. NSSM still couldn't start, the bat never got a chance to run.
This further confirmed it was NSSM itself, not the bat content.
5. Fourth Attempt: NSSM + python.exe Full Path (Failed)
Final throw: skip the bat, NSSM Application = python.exe full path, AppParameters = app.py absolute path.
cmd /c 'nssm install BaccAI_PHD "C:\Users\Administrator\AppData\Local\Programs\Python\Python38\python.exe"'
cmd /c 'nssm set BaccAI_PHD AppParameters "D:\phd824\app.py"'
cmd /c "nssm set BaccAI_PHD AppDirectory D:\phd824"
cmd /c "nssm start BaccAI_PHD"
Result: same error.
This conclusively proved NSSM was broken on this machine, unrelated to Python path, bat path, or permissions.
6. Breakthrough: Switched to schtasks.exe (5 Minutes)
Suddenly remembered: Windows has native Task Scheduler, command-line tool is schtasks.exe. No third-party exe needed, Windows built-in, and config is 10x simpler than NSSM.
Key insight: Task Scheduler tasks run under SYSTEM account by default with full admin permissions. The death loop I implement in my own bat, not relying on NSSM's AppExit Restart policy.
Step 1: Write the Watchdog Bat
Simple idea: infinite loop, launch Python, when Python dies wait 5 seconds and launch again.
@echo off
:loop
echo [%date% %time%] PHD starting...
cd /d D:\phd824
C:\Users\Administrator\AppData\Local\Programs\Python\Python38\python.exe app.py 1>>D:\phd824\app.log 2>>D:\phd824\app_err.log
echo [%date% %time%] PHD exited, restarting in 5s
timeout /t 5 /nobreak >nul
goto loop
Old PRO gets a similar bat, just different path.
Key design points:
- :loop / goto loop = infinite loop
- timeout /t 5 /nobreak = wait 5 seconds, avoid crazy restart loops
- 1>>log 2>>err.log = redirect stdout/stderr to files for diagnosis
- cd /d = switch to working directory so Python relative paths work
Total bat is under 10 lines.
Step 2: schtasks.exe Register Boot Tasks
Way simpler than NSSM:
schtasks /Create /TN "BaccAI_PHD" /TR "D:\phd824\run_phd_loop.bat" /SC ONSTART /RL HIGHEST /RU SYSTEM /F
schtasks /Create /TN "BaccAI_PRO" /TR "D:\20260516\vb_bendi_v24\run_pro_loop.bat" /SC ONSTART /RL HIGHEST /RU SYSTEM /F
Parameter explanation:
/TN= Task Name/TR= Task Run (command to execute)/SC ONSTART= trigger: on system boot/RL HIGHEST= run with highest privileges/RU SYSTEM= run as SYSTEM account/F= force overwrite (overwrite if task exists)
Launch immediately after registration:
schtasks /Run /TN "BaccAI_PHD"
schtasks /Run /TN "BaccAI_PRO"
Step 3: Verify
netstat -an | findstr ":8443 :8000 "
Output:
TCP 0.0.0.0:8443 0.0.0.0:0 LISTENING
TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING
Both Flask apps running. Opened D:\phd824\app_err.log:
* Running on https://0.0.0.0:8443
* Running on http://127.0.0.1:8080
Standard Flask startup info. Done.
7. Watchdog Self-Healing Test (5-Second Recovery)
After setting up tasks, I manually killed PHD to verify watchdog really restarts:
Get-Process python | Where-Object {$_.Id -eq 51524} | Stop-Process -Force
Start-Sleep -Seconds 10
Get-Process | Where-Object {$_.ProcessName -eq "python"} | Select Id, StartTime
10 seconds later, check: python process StartTime is new, Id changed. Watchdog bat auto-restarted Python after the 5-second timeout.
Killed 3 times in a row, all auto-restarted. Watchdog works.
8. Health Check: Email Alert on Death
Even with watchdog auto-restart, the service is briefly unavailable for 5 seconds. If Flask startup itself hangs (e.g., database connection fails), watchdog keeps restarting uselessly.
I added a 5-minute health check:
# D:\seo\healthcheck.py
import urllib.request
import socket
import json
from datetime import datetime
def check_port(host, port):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
s.connect((host, port))
s.close()
return True
except Exception:
return False
def send_alert(subject, body):
req = urllib.request.Request(
"https://api.resend.com/emails",
data=json.dumps({
"from": "alert@baccai.com",
"to": ["your-email@example.com"],
"subject": subject,
"text": body
}).encode("utf-8"),
headers={
"Authorization": "Bearer re_xxxxxxxxxxxx",
"Content-Type": "application/json"
}
)
urllib.request.urlopen(req, timeout=10)
# Check PHD 8443 + PRO 8000
results = []
for name, port in [("PHD", 8443), ("PRO", 8000)]:
if not check_port("www.baccpc.com", port):
results.append(f"DOWN: {name}:{port}")
if results:
send_alert(
"[BaccAI Alert] " + str(len(results)) + " service DOWN",
"\n".join(results) + "\n\nTime: " + str(datetime.now())
)
Register as 5-minute task:
schtasks /Create /TN "BaccAI_HealthCheck" /TR "C:\Users\...\python.exe D:\seo\healthcheck.py" /SC MINUTE /MO 5 /RL HIGHEST /RU SYSTEM /F
Now if any Flask stays down for more than 5 minutes, I get an email alert. No more log watching.
9. Why Did NSSM Not Work on My Machine?
After Sept 8 investigation, I have a rough guess (not 100% confirmed):
- Windows Store Python has special ACL:
C:\Users\Administrator\AppData\Local\Programs\Python\Python38\has many AppContainer SIDs (WindowsApps identifiers) besides Administrator and SYSTEM. LocalSystem (NSSM default) conflicts with these SIDs. - WDAC (Windows Defender Application Control) policy: server may have an app whitelist that disallows LocalSystem from launching Python from non-system drives.
- NSSM version compatibility: 2.24 has reports of similar issues on Windows Server 2019 / 2022.
Doesn't matter anymore. Switched to schtasks, problem disappeared. NSSM fans might say "you used the wrong version" — but I have no grudge against NSSM, the tool just couldn't be made to work in 4 hours here, and schtasks solved it in 5 minutes. I choose what saves time.
10. Complete Start Reference Card (Saved to D:\seo\START_COMMANDS.txt)
To prevent future me (or whoever inherits this) from re-discovering all this, I consolidated all commands into one file at D:\seo\START_COMMANDS.txt on the server. 4 core steps:
Step 1: Status Check
netstat -an | findstr ":8443 :8000 :8080"
Get-Process python | Select Id, StartTime
Step 2: Manual Launch (One-time, no loop)
:: PHD
cd /d D:\phd824
C:\Users\Administrator\AppData\Local\Programs\Python\Python38\python.exe app.py
:: PRO
cd /d D:\20260516\vb_bendi_v24
C:\Users\Administrator\AppData\Local\Programs\Python\Python38\python.exe app.py
Step 3: Watchdog Launch (Production, recommended)
Start-Process D:\phd824\run_phd_loop.bat
Start-Process D:\20260516\vb_bendi_v24\run_pro_loop.bat
Step 4: Emergency Restart
Get-Process python -ErrorAction SilentlyContinue | Stop-Process -Force
timeout /t 5 /nobreak
Start-Process D:\phd824\run_phd_loop.bat
Start-Process D:\20260516\vb_bendi_v24\run_pro_loop.bat
timeout /t 15 /nobreak
netstat -an | findstr ":8443 :8000 "
Any time service dies, 30 seconds to recover.