<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Wick</title>
    <description>The latest articles on DEV Community by John Wick (@codenamew).</description>
    <link>https://dev.to/codenamew</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4043176%2Fc4e85b98-139d-4c0c-a3be-1988390f4d72.jpg</url>
      <title>DEV Community: John Wick</title>
      <link>https://dev.to/codenamew</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codenamew"/>
    <language>en</language>
    <item>
      <title>Run Python Bots Without Sleep On Railway With StayPresent</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:39:43 +0000</pubDate>
      <link>https://dev.to/codenamew/run-python-bots-without-sleep-on-railway-with-staypresent-3jnc</link>
      <guid>https://dev.to/codenamew/run-python-bots-without-sleep-on-railway-with-staypresent-3jnc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Deploy a railway python bot that never sleeps — port binding, health checks, self-ping, and crash recovery using StayPresent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Run Python Bots Without Sleep On Railway With StayPresent
&lt;/h2&gt;

&lt;p&gt;Railway auto-detects Python projects and gets you deployed in minutes, but a &lt;strong&gt;railway python bot&lt;/strong&gt; — a Discord bot, Telegram bot, or scraper — still runs into the same core issue every PaaS presents: the platform expects an HTTP-facing process, and your bot's event loop or gateway connection doesn't naturally provide one. This guide covers deploying a bot to Railway that stays online, using StayPresent to fill that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How Railway Detects and Runs Python Apps&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;$PORT&lt;/code&gt; Requirement&lt;/li&gt;
&lt;li&gt;Setting Up StayPresent&lt;/li&gt;
&lt;li&gt;Health Checks in Railway's Dashboard&lt;/li&gt;
&lt;li&gt;Self-Ping for Inactivity-Sensitive Plans&lt;/li&gt;
&lt;li&gt;Crash Recovery&lt;/li&gt;
&lt;li&gt;Full Working Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Railway Detects and Runs Python Apps
&lt;/h2&gt;

&lt;p&gt;Railway auto-detects a Python project from &lt;code&gt;requirements.txt&lt;/code&gt; (or &lt;code&gt;pyproject.toml&lt;/code&gt;) and builds a container for it automatically via Nixpacks, without requiring a Dockerfile. It then starts your app and expects it to bind to the port Railway assigns via the &lt;code&gt;PORT&lt;/code&gt; environment variable, so its own routing and health checks can reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;$PORT&lt;/code&gt; Requirement
&lt;/h2&gt;

&lt;p&gt;This is the same pattern seen across virtually every modern PaaS: read &lt;code&gt;PORT&lt;/code&gt; from the environment rather than hardcoding it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your bot never binds to this port at all — which is normal, expected behavior for a Discord or Telegram bot — Railway's health checks (if enabled) will report the deployment as unhealthy, even though your bot might be functioning perfectly on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up StayPresent
&lt;/h2&gt;

&lt;p&gt;Install the production extra so Waitress handles serving instead of Flask's development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-telegram-bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Railway's start command is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Health Checks in Railway's Dashboard
&lt;/h2&gt;

&lt;p&gt;If you've enabled Railway's health check feature for a service, you can point it specifically at StayPresent's built-in &lt;code&gt;/health&lt;/code&gt; endpoint, which always returns &lt;code&gt;{"status": "ok"}&lt;/code&gt; — independent of whatever you've configured at your root route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# /health still returns {"status": "ok"} regardless
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separates platform-level health monitoring from any custom status payload you're serving to humans or your own tooling at &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Ping for Inactivity-Sensitive Plans
&lt;/h2&gt;

&lt;p&gt;Whether inactivity-based sleeping applies depends on your specific Railway plan and configuration — always check Railway's current pricing and usage documentation before assuming behavior either way. If it does apply to your deployment, &lt;code&gt;staypresent.cron()&lt;/code&gt; generates outbound traffic against your own public URL to keep it from being treated as idle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-app.up.railway.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with any platform, this must target the actual public URL — pinging &lt;code&gt;0.0.0.0&lt;/code&gt; or &lt;code&gt;127.0.0.1&lt;/code&gt; never leaves the machine and has no effect on inactivity detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash Recovery
&lt;/h2&gt;

&lt;p&gt;Bots crash for mundane reasons, and Railway won't automatically relaunch a subprocess that dies &lt;em&gt;inside&lt;/em&gt; your container unless the container itself exits. StayPresent handles that inner layer directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_reset_after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If restarts are ultimately exhausted, StayPresent exits the whole process with the bot's original exit code — letting Railway's own deployment restart policy act as a final backstop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Working Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-app.up.railway.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always read &lt;code&gt;PORT&lt;/code&gt; dynamically — Railway assigns it per-deployment and it can differ between environments.&lt;/li&gt;
&lt;li&gt;Point any configured health check at &lt;code&gt;/health&lt;/code&gt; rather than &lt;code&gt;/&lt;/code&gt; if your root route serves something custom.&lt;/li&gt;
&lt;li&gt;Keep the &lt;code&gt;prod&lt;/code&gt; extra installed for any deployment receiving real traffic, not just keep-alive pings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding &lt;code&gt;port=8080&lt;/code&gt;&lt;/strong&gt; instead of reading &lt;code&gt;os.getenv("PORT", 8080)&lt;/code&gt; — this can work in some configurations and silently fail in others depending on how Railway is set up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming Railway's sleep behavior is identical to Render's.&lt;/strong&gt; Policies differ between platforms and change over time — verify current behavior in Railway's own docs rather than assuming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping crash recovery because "Railway restarts crashed deployments anyway."&lt;/strong&gt; Railway's own restart policy operates at the container level; a bot subprocess crashing inside a still-running container is a separate failure mode StayPresent handles directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Railway require a Dockerfile?&lt;/strong&gt;&lt;br&gt;
No — Railway can auto-detect and build Python projects via Nixpacks without one, though a Dockerfile is also supported if you prefer explicit control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I reuse the same &lt;code&gt;main.py&lt;/code&gt; from a Render deployment?&lt;/strong&gt;&lt;br&gt;
Yes — since both platforms follow the same &lt;code&gt;$PORT&lt;/code&gt; convention, the identical entry point typically works unchanged across both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need &lt;code&gt;staypresent.cron()&lt;/code&gt; on Railway?&lt;/strong&gt;&lt;br&gt;
Only if your specific plan/configuration is subject to inactivity-based sleeping — check Railway's current documentation for your plan before deciding.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;railway python bot&lt;/strong&gt; stays online the same way a bot on any modern PaaS does: bind to the assigned port, expose a dedicated health endpoint, and let StayPresent supervise the bot process itself. The setup is close to identical across Railway, Render, Koyeb, and Heroku, which is exactly what makes StayPresent a one-time investment rather than per-platform boilerplate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>cloud</category>
      <category>deployment</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>StayPresent vs Supervisor vs PM2 for Python Bot Process Management</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:38:52 +0000</pubDate>
      <link>https://dev.to/codenamew/staypresent-vs-supervisor-vs-pm2-for-python-bot-process-management-4527</link>
      <guid>https://dev.to/codenamew/staypresent-vs-supervisor-vs-pm2-for-python-bot-process-management-4527</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Comparing StayPresent, Supervisor, and PM2 for python process management — setup, crash recovery, HTTP port handling, and when each fits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  StayPresent vs Supervisor vs PM2 for Python Bot Process Management
&lt;/h2&gt;

&lt;p&gt;Supervisor and PM2 are two of the most established tools for &lt;strong&gt;python process management&lt;/strong&gt; — keeping a process running, restarting it on crash, managing logs. Both predate StayPresent by years and are proven, widely-used tools. But neither was built with the specific PaaS-deployment problem in mind: satisfying a hosting platform's HTTP port requirement. Here's how all three actually compare for the common case of deploying a bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What Each Tool Is Actually For&lt;/li&gt;
&lt;li&gt;Installation and Setup&lt;/li&gt;
&lt;li&gt;Configuration Style&lt;/li&gt;
&lt;li&gt;Crash Recovery Comparison&lt;/li&gt;
&lt;li&gt;The HTTP Port Problem&lt;/li&gt;
&lt;li&gt;Logging Comparison&lt;/li&gt;
&lt;li&gt;Comparison Table&lt;/li&gt;
&lt;li&gt;When Each Tool Is the Right Choice&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Each Tool Is Actually For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Supervisor&lt;/strong&gt; is a mature, Python-based process control system, originally designed for Unix-like systems, that manages arbitrary processes via a config file and a control daemon (&lt;code&gt;supervisord&lt;/code&gt;). It's general-purpose — it doesn't care whether the process is Python, Node, or a shell script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PM2&lt;/strong&gt; is a Node.js process manager, widely used for Node apps but also commonly used to run Python scripts, offering restart-on-crash, log management, and a built-in monitoring dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;StayPresent&lt;/strong&gt; is a Python package, not a system-level daemon — it runs inside your own Python process, launching your bot as a subprocess it directly supervises, while also running an HTTP server specifically to satisfy PaaS hosting requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and Setup
&lt;/h2&gt;

&lt;p&gt;Supervisor requires a system-level install and a separate config file, typically outside your project's own codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; /etc/supervisor/conf.d/mybot.conf
&lt;/span&gt;&lt;span class="nn"&gt;[program:mybot]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;python bot.py&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;supervisor
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl reread
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PM2 requires Node.js itself as a dependency, even to run a Python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
pm2 start bot.py &lt;span class="nt"&gt;--interpreter&lt;/span&gt; python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;StayPresent is a pure Python dependency, installed and configured entirely within your own script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration Style
&lt;/h2&gt;

&lt;p&gt;Supervisor's configuration lives in a separate &lt;code&gt;.ini&lt;/code&gt;-style file, generally outside version control unless you specifically arrange for it (and outside your application's own deploy artifact on most PaaS platforms, which don't give you system-level config access at all). PM2 configuration can live in an &lt;code&gt;ecosystem.config.js&lt;/code&gt; file or be passed via CLI flags. StayPresent's configuration is just Python function arguments, living directly in your own entry-point script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters specifically on PaaS platforms like Render, Railway, or Koyeb, which typically don't give you access to install and configure a system daemon like Supervisor at all — your deployment is just "run this command," which is exactly the model StayPresent fits into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash Recovery Comparison
&lt;/h2&gt;

&lt;p&gt;All three tools handle crash recovery, though with different granularity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supervisor&lt;/strong&gt; supports &lt;code&gt;autorestart&lt;/code&gt;, &lt;code&gt;startretries&lt;/code&gt;, and backoff timing, configured per-program in its &lt;code&gt;.ini&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PM2&lt;/strong&gt; supports &lt;code&gt;--restart-delay&lt;/code&gt;, &lt;code&gt;max_restarts&lt;/code&gt;, and exponential backoff, configured via CLI flags or its ecosystem file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StayPresent&lt;/strong&gt; supports &lt;code&gt;restart_on_crash&lt;/code&gt;, &lt;code&gt;max_restarts&lt;/code&gt;, &lt;code&gt;restart_delay&lt;/code&gt;, and &lt;code&gt;restart_reset_after&lt;/code&gt; — the last of which specifically resets the crash counter after a period of sustained uptime, avoiding a slow accumulation toward the restart ceiling from unrelated, infrequent crashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Functionally, all three solve the core problem. The difference is entirely about &lt;em&gt;where&lt;/em&gt; that configuration lives and what environment it assumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTTP Port Problem
&lt;/h2&gt;

&lt;p&gt;This is the actual differentiator for PaaS deployment specifically. Neither Supervisor nor PM2 has any concept of an HTTP health-check port — they're process managers, full stop. If your hosting platform requires a bound HTTP port to consider a deployment healthy, using Supervisor or PM2 alone still leaves you writing a separate Flask (or equivalent) server yourself, exactly like the "custom Flask server" approach covered in other comparisons.&lt;/p&gt;

&lt;p&gt;StayPresent bundles both concerns into one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# HTTP server + process supervision, together
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Logging Comparison
&lt;/h2&gt;

&lt;p&gt;Supervisor writes process stdout/stderr to log files it manages itself, configurable per-program. PM2 provides its own log aggregation and a &lt;code&gt;pm2 logs&lt;/code&gt; command. StayPresent logs its own orchestration events (crashes, restarts, shutdowns) through a dedicated &lt;code&gt;"staypresent"&lt;/code&gt; Python logger that never touches the root logger — your bot's own stdout/stderr, meanwhile, is inherited normally from the subprocess, the same as it would be under Supervisor or PM2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Supervisor&lt;/th&gt;
&lt;th&gt;PM2&lt;/th&gt;
&lt;th&gt;StayPresent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requires system-level install&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (+ Node.js)&lt;/td&gt;
&lt;td&gt;No — pure Python package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on most PaaS platforms&lt;/td&gt;
&lt;td&gt;Rarely (no daemon access)&lt;/td&gt;
&lt;td&gt;Rarely (no daemon access)&lt;/td&gt;
&lt;td&gt;Yes, by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in HTTP health endpoint&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config location&lt;/td&gt;
&lt;td&gt;Separate &lt;code&gt;.ini&lt;/code&gt; file&lt;/td&gt;
&lt;td&gt;CLI / ecosystem file&lt;/td&gt;
&lt;td&gt;Python code in your script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash counter reset after uptime&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial (backoff-based)&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;restart_reset_after&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language dependency&lt;/td&gt;
&lt;td&gt;Python-agnostic&lt;/td&gt;
&lt;td&gt;Requires Node.js&lt;/td&gt;
&lt;td&gt;Native Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When Each Tool Is the Right Choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Supervisor&lt;/strong&gt; remains an excellent choice on a VPS or bare-metal server where you have full system access and want to manage several unrelated processes (not just Python) under one consistent tool. &lt;strong&gt;PM2&lt;/strong&gt; makes sense if you're already running a Node.js stack alongside a Python script and want one process manager for both. &lt;strong&gt;StayPresent&lt;/strong&gt; is the better fit specifically for PaaS deployments — Render, Railway, Koyeb, Heroku — where you don't have system daemon access at all, and where the HTTP port requirement is the actual problem you're solving, not just process supervision in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use Supervisor or PM2 together with StayPresent?&lt;/strong&gt;&lt;br&gt;
On a VPS, yes — Supervisor or PM2 could supervise the outer &lt;code&gt;python main.py&lt;/code&gt; process (which itself runs StayPresent), giving you a second, outer restart layer on top of StayPresent's own bot-subprocess supervision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does StayPresent replace Supervisor entirely on a VPS?&lt;/strong&gt;&lt;br&gt;
Not necessarily — Supervisor's system-level features (managing multiple unrelated services, log rotation, a control CLI) go beyond what StayPresent aims to do, which is specifically bot-process supervision plus an HTTP port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is PM2 a reasonable choice for a pure-Python project?&lt;/strong&gt;&lt;br&gt;
It works, but it introduces a Node.js dependency purely to manage a Python process — StayPresent avoids that entirely for projects that are otherwise pure Python.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Supervisor and PM2 are solid, mature tools for &lt;strong&gt;python process management&lt;/strong&gt; in environments where you control the underlying system. But most PaaS platforms don't grant that kind of access, and neither tool addresses the HTTP-port health-check requirement those platforms impose. StayPresent solves both problems together, as a pure Python dependency that fits directly into your bot's own entry point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>automation</category>
      <category>devops</category>
      <category>python</category>
      <category>tools</category>
    </item>
    <item>
      <title>Run Python Bots Without Sleep On Koyeb With StayPresent</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:36:50 +0000</pubDate>
      <link>https://dev.to/codenamew/run-python-bots-without-sleep-on-koyeb-with-staypresent-53bl</link>
      <guid>https://dev.to/codenamew/run-python-bots-without-sleep-on-koyeb-with-staypresent-53bl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Deploy a koyeb python bot with proper health checks, port binding, and crash recovery using StayPresent — Git and Docker deploy paths covered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Run Python Bots Without Sleep On Koyeb With StayPresent
&lt;/h2&gt;

&lt;p&gt;Koyeb supports deploying straight from a Git repository or from a Dockerfile, and like most serverless-leaning PaaS platforms, it evaluates your service's health through HTTP checks against a port it expects your app to bind. A &lt;strong&gt;koyeb python bot&lt;/strong&gt; — a Discord bot, Telegram bot, or background worker — needs that same small HTTP layer every other platform in this series requires, and StayPresent provides it with the same one-line setup regardless of which Koyeb deploy path you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Two Ways to Deploy on Koyeb&lt;/li&gt;
&lt;li&gt;The Port Binding Requirement&lt;/li&gt;
&lt;li&gt;Setting Up StayPresent&lt;/li&gt;
&lt;li&gt;Configuring Koyeb's Health Check Path&lt;/li&gt;
&lt;li&gt;Self-Ping Considerations&lt;/li&gt;
&lt;li&gt;Crash Recovery&lt;/li&gt;
&lt;li&gt;Full Working Example (Git-Based)&lt;/li&gt;
&lt;li&gt;Full Working Example (Docker-Based)&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two Ways to Deploy on Koyeb
&lt;/h2&gt;

&lt;p&gt;Koyeb supports two primary deployment paths for a Python service: a &lt;strong&gt;Git-based buildpack deploy&lt;/strong&gt;, where Koyeb detects and builds your app automatically from &lt;code&gt;requirements.txt&lt;/code&gt;, and a &lt;strong&gt;Dockerfile-based deploy&lt;/strong&gt;, where you provide the full container definition yourself. Both ultimately run the same command you specify and expect the same thing from it: a bound HTTP port.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Port Binding Requirement
&lt;/h2&gt;

&lt;p&gt;Koyeb injects a &lt;code&gt;PORT&lt;/code&gt; environment variable, exactly like Render, Railway, and Heroku:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without something bound to that port, Koyeb's health checks will report the service as unhealthy — independent of whether your bot itself is running perfectly fine internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up StayPresent
&lt;/h2&gt;

&lt;p&gt;For a Git-based deploy, add the production extra to &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And set your &lt;code&gt;main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Koyeb's run command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring Koyeb's Health Check Path
&lt;/h2&gt;

&lt;p&gt;By default, a platform's health check often targets &lt;code&gt;/&lt;/code&gt;. StayPresent's built-in &lt;code&gt;/health&lt;/code&gt; endpoint — which always returns &lt;code&gt;{"status": "ok"}&lt;/code&gt; regardless of what's configured at your root route — is generally the better target, since it stays stable even if you later change what &lt;code&gt;/&lt;/code&gt; serves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# /health remains {"status": "ok"} independently
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Koyeb's service settings, set the health check path explicitly to &lt;code&gt;/health&lt;/code&gt; rather than relying on the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Ping Considerations
&lt;/h2&gt;

&lt;p&gt;Koyeb's specific behavior around scaling-to-zero or idle handling depends on the service type and plan you've configured — check Koyeb's current documentation for your particular setup rather than assuming. If your deployment is subject to any form of inactivity-based scaling you want to avoid, &lt;code&gt;staypresent.cron()&lt;/code&gt; can generate outbound traffic against your own public URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-app.koyeb.app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Crash Recovery
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If restarts are ultimately exhausted, StayPresent exits the process with the bot's original exit code, letting Koyeb's own deployment-level restart behavior serve as a final backstop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Working Example (Git-Based)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# requirements.txt
staypresent[prod]
python-telegram-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Full Working Example (Docker-Based)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "main.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main.py&lt;/code&gt; is identical to the Git-based example above — the deploy mechanism changes, the StayPresent configuration doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set the health check path to &lt;code&gt;/health&lt;/code&gt; explicitly in Koyeb's dashboard rather than leaving it on a default that may target &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use the Docker deploy path if your bot has system-level dependencies (e.g. &lt;code&gt;ffmpeg&lt;/code&gt;) that a buildpack deploy won't install automatically.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;main.py&lt;/code&gt; platform-agnostic (reading &lt;code&gt;PORT&lt;/code&gt; from the environment, targeting the correct public URL for &lt;code&gt;cron()&lt;/code&gt;) so the same file works unchanged if you later move to Render or Railway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leaving the health check path on a default that doesn't match what your root route actually serves&lt;/strong&gt;, causing false-negative health failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming a Git-based deploy and a Docker-based deploy require different StayPresent configuration.&lt;/strong&gt; They don't — only the deployment mechanism around &lt;code&gt;main.py&lt;/code&gt; changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;ffmpeg&lt;/code&gt;, &lt;code&gt;ImageMagick&lt;/code&gt;, or other system packages&lt;/strong&gt; a bot might depend on, which a plain Git buildpack deploy won't install — this calls for the Docker path instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Koyeb require a Dockerfile?&lt;/strong&gt;&lt;br&gt;
No — Git-based buildpack deploys work for standard Python projects without one; Docker is only needed for more customized environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the exact same &lt;code&gt;main.py&lt;/code&gt; across Koyeb, Render, and Railway?&lt;/strong&gt;&lt;br&gt;
Yes, as long as &lt;code&gt;PORT&lt;/code&gt; is read from the environment and any self-ping URL is configured per-platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;/health&lt;/code&gt; need to be manually created?&lt;/strong&gt;&lt;br&gt;
No — it's provisioned automatically by StayPresent unless you explicitly register your own response there.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;koyeb python bot&lt;/strong&gt;, whether deployed via Git or Docker, needs the same thing every PaaS bot deployment needs: a bound HTTP port and a reliable health check target. StayPresent provides both with an identical, minimal setup regardless of which Koyeb deploy path you choose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>automation</category>
      <category>devops</category>
      <category>docker</category>
      <category>python</category>
    </item>
    <item>
      <title>StayPresent vs Pulsetic vs Custom Flask Server for Python Bots</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:41:59 +0000</pubDate>
      <link>https://dev.to/codenamew/staypresent-vs-pulsetic-vs-custom-flask-server-for-python-bots-4o5b</link>
      <guid>https://dev.to/codenamew/staypresent-vs-pulsetic-vs-custom-flask-server-for-python-bots-4o5b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Comparing StayPresent, Pulsetic, and a hand-rolled Flask server for keeping a python bot alive — external monitoring vs in-process supervision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  StayPresent vs Pulsetic vs Custom Flask Server for Python Bots
&lt;/h2&gt;

&lt;p&gt;A comment on an earlier comparison post asked how StayPresent stacks up against Pulsetic specifically, rather than UptimeRobot — a fair question, since Pulsetic has grown into a genuinely feature-rich external monitoring service in its own right. This post covers the same three-way comparison — an external monitor, a hand-rolled Flask server, and StayPresent — but with Pulsetic's specific feature set in view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What Pulsetic Actually Is&lt;/li&gt;
&lt;li&gt;What Each Approach Solves&lt;/li&gt;
&lt;li&gt;Monitoring Depth: Where Pulsetic Pulls Ahead&lt;/li&gt;
&lt;li&gt;Crash Recovery: Where Pulsetic Can't Help&lt;/li&gt;
&lt;li&gt;Status Pages vs a Bot Status Endpoint&lt;/li&gt;
&lt;li&gt;Cost and Free-Tier Limits&lt;/li&gt;
&lt;li&gt;Comparison Table&lt;/li&gt;
&lt;li&gt;Using Pulsetic and StayPresent Together&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Pulsetic Actually Is
&lt;/h2&gt;

&lt;p&gt;Pulsetic is an external website and service uptime monitor, checking your endpoint from &lt;cite&gt;multiple global locations every 30 seconds and alerting through phone calls, SMS, email, Slack, and other channels&lt;/cite&gt; when something goes down. Beyond bare uptime, it supports &lt;cite&gt;HTTP, ping, port, keyword, and heartbeat monitoring&lt;/cite&gt;, along with &lt;cite&gt;SSL certificate expiration alerts and scheduled maintenance windows&lt;/cite&gt; for planned deploys. It also offers &lt;cite&gt;customizable status pages with custom domains and password protection&lt;/cite&gt;, aimed more at communicating uptime to end users than at keeping a background process alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Each Approach Solves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pulsetic&lt;/strong&gt; is purely external — it never runs inside your bot's process. It watches your endpoint from the outside and tells you (and optionally your users, via a status page) when something's wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A custom Flask server&lt;/strong&gt; runs inside your process, alongside your bot, purely to give a hosting platform something to health-check. It doesn't monitor from the outside or alert anyone by itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;StayPresent&lt;/strong&gt; also runs inside your process, but goes further than a bare Flask server: it manages your bot as a supervised subprocess, restarts it on crash, and can optionally self-ping your own public URL to prevent host-side inactivity sleep.&lt;/p&gt;

&lt;p&gt;These aren't fully overlapping tools — Pulsetic answers "is my bot reachable, and who do I tell if it isn't," while StayPresent answers "how do I keep my bot itself running, and satisfy my host's port requirement in the process."&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Depth: Where Pulsetic Pulls Ahead
&lt;/h2&gt;

&lt;p&gt;If your priority is genuinely rich external monitoring — response-time trends over time, &lt;cite&gt;detailed uptime reports with response times from different locations worldwide&lt;/cite&gt;, and alerting across several channels at once — Pulsetic goes considerably deeper than anything StayPresent attempts. StayPresent's &lt;code&gt;ping()&lt;/code&gt;/&lt;code&gt;cron()&lt;/code&gt; are intentionally simple: a GET request, a status code, an elapsed time, and an optional success/failure callback. They're built for keep-warm behavior, not for the kind of historical reporting and multi-location checking Pulsetic specializes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash Recovery: Where Pulsetic Can't Help
&lt;/h2&gt;

&lt;p&gt;This is the same limitation any external monitor shares: Pulsetic can tell you your bot went down, but it has no mechanism to bring it back up — it doesn't run inside your process and has no access to your bot's subprocess. StayPresent restarts a crashed bot automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A team relying on Pulsetic alone still needs to be the one who notices the alert and manually restarts the service — or pairs it with something like StayPresent that actually manages the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status Pages vs a Bot Status Endpoint
&lt;/h2&gt;

&lt;p&gt;Pulsetic's &lt;cite&gt;status pages with logos, brand colors, and a custom domain&lt;/cite&gt; are built for communicating with end users during an incident — genuinely useful if your bot has a customer-facing component people check on. StayPresent's &lt;code&gt;web.json()&lt;/code&gt;/&lt;code&gt;web.html()&lt;/code&gt; responses are a much lighter-weight equivalent: a status payload or dashboard served directly from your bot's own process, useful for your own debugging rather than public-facing incident communication. If you need the latter, Pulsetic's status pages are the more purpose-built tool; if you just need somewhere to glance at your bot's own internal state, StayPresent's built-in responses are enough on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and Free-Tier Limits
&lt;/h2&gt;

&lt;p&gt;Pulsetic's free tier covers &lt;cite&gt;50 monitors and 5-minute check intervals&lt;/cite&gt;, with faster intervals and advanced features starting around &lt;cite&gt;$7.50 a month&lt;/cite&gt;. StayPresent is MIT licensed and free regardless of scale — but it's solving a different problem, so the comparison isn't quite apples-to-apples: Pulsetic's pricing reflects the cost of running a global monitoring network from the outside, while StayPresent's functionality runs entirely inside a process you already control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Pulsetic&lt;/th&gt;
&lt;th&gt;Custom Flask&lt;/th&gt;
&lt;th&gt;StayPresent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs inside your bot's process&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-location external checks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash recovery for your bot&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-ping / keep-warm&lt;/td&gt;
&lt;td&gt;N/A (is the external checker)&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public-facing status pages&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Basic (&lt;code&gt;web.html&lt;/code&gt;/&lt;code&gt;web.json&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL certificate monitoring&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Yes, limited&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Fully free (MIT)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Using Pulsetic and StayPresent Together
&lt;/h2&gt;

&lt;p&gt;These tools aren't mutually exclusive, and combining them covers more ground than either alone: point Pulsetic at StayPresent's &lt;code&gt;/health&lt;/code&gt; endpoint for genuine external, multi-location alerting and SSL monitoring, while StayPresent's own &lt;code&gt;restart_on_crash&lt;/code&gt; and &lt;code&gt;cron()&lt;/code&gt; handle the parts Pulsetic structurally can't — actually restarting a crashed bot process and generating keep-warm traffic from inside your own deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# Point Pulsetic's monitor at https://your-app.example.com/health
&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Pulsetic replace StayPresent's keep-alive HTTP server?&lt;/strong&gt;&lt;br&gt;
No — Pulsetic checks a URL from the outside; it doesn't provide anything for your hosting platform's own port-binding health check to reach. You still need something like StayPresent (or a custom server) running inside your deployment for that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Pulsetic restart a crashed bot?&lt;/strong&gt;&lt;br&gt;
No — external monitors like Pulsetic can only detect and alert on downtime, not restart the underlying process, since they have no access to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Pulsetic worth using if I already have StayPresent's &lt;code&gt;cron()&lt;/code&gt; self-ping?&lt;/strong&gt;&lt;br&gt;
They serve different purposes — &lt;code&gt;cron()&lt;/code&gt; is about preventing host-side inactivity sleep; Pulsetic is about knowing (and telling others) when something is actually broken, from an outside vantage point.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Pulsetic and StayPresent aren't really competing for the same job. Pulsetic is a genuinely capable external monitoring and status-page service; StayPresent is an in-process bot supervisor and keep-alive HTTP server. For a python bot deployment, the strongest setup is usually both together — StayPresent keeping the bot itself alive and restarting it on crash, Pulsetic watching from the outside and telling you (or your users) when something still goes wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>python</category>
      <category>tools</category>
    </item>
    <item>
      <title>Security Notes for Serving Static Files with StayPresent</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:35:04 +0000</pubDate>
      <link>https://dev.to/codenamew/security-notes-for-serving-static-files-with-staypresent-mae</link>
      <guid>https://dev.to/codenamew/security-notes-for-serving-static-files-with-staypresent-mae</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;What to know about python static file security when using StayPresent's web.html/markdown — directory exposure, path traversal, and URL filtering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Security Notes for Serving Static Files with StayPresent
&lt;/h2&gt;

&lt;p&gt;Serving a status dashboard or a rendered README with &lt;code&gt;web.html()&lt;/code&gt;/&lt;code&gt;web.markdown()&lt;/code&gt; is convenient precisely because it automatically picks up neighboring CSS, JS, and images with no extra configuration. That same convenience has a security dimension worth understanding clearly before you point it at a directory. This covers &lt;strong&gt;python static file security&lt;/strong&gt; as it applies specifically to StayPresent: what's protected automatically, and what's still your responsibility to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The Directory-Wide Exposure Behavior&lt;/li&gt;
&lt;li&gt;Why This Is Intentional&lt;/li&gt;
&lt;li&gt;Path Traversal Protection&lt;/li&gt;
&lt;li&gt;The One-Time Directory Warning&lt;/li&gt;
&lt;li&gt;Markdown-Specific Protections: Escaping&lt;/li&gt;
&lt;li&gt;Markdown-Specific Protections: URL Scheme Filtering&lt;/li&gt;
&lt;li&gt;What's Rejected vs What's Allowed&lt;/li&gt;
&lt;li&gt;Structuring Directories Safely&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Directory-Wide Exposure Behavior
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;web.html("templates/index.html")&lt;/code&gt; or &lt;code&gt;web.markdown("docs/guide.md")&lt;/code&gt;, StayPresent doesn't just serve that one file — it serves &lt;strong&gt;every file in that file's directory&lt;/strong&gt;, not only the specific CSS/JS/image files actually referenced from the page. This is what makes relative asset links (&lt;code&gt;href="style.css"&lt;/code&gt;, &lt;code&gt;src="images/logo.png"&lt;/code&gt;) work automatically without any extra configuration on your part.&lt;/p&gt;

&lt;p&gt;The consequence: if a &lt;code&gt;.env&lt;/code&gt; file, your bot's own source code, or a &lt;code&gt;.git/&lt;/code&gt; directory happens to sit in that same directory, it becomes downloadable by anyone who requests it by name — whether or not anything on the page actually links to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;templates/
├── index.html
├── style.css        &amp;lt;- intentionally public, referenced from index.html
├── .env              &amp;lt;- NOT referenced anywhere, but still reachable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Is Intentional
&lt;/h2&gt;

&lt;p&gt;This isn't an oversight — it's what makes &lt;code&gt;web.html()&lt;/code&gt;/&lt;code&gt;web.markdown()&lt;/code&gt; usable with zero configuration for the overwhelming majority of legitimate cases (a template directory containing only the page and its assets). Requiring an explicit allowlist of every servable file for every deployment would defeat the "read fresh from disk" simplicity that makes these two functions useful in the first place. StayPresent's own documentation notes that an opt-in allowlist restricting exposure to only &lt;em&gt;referenced&lt;/em&gt; files is being considered for a future release — but as things stand, the responsibility for directory contents is on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path Traversal Protection
&lt;/h2&gt;

&lt;p&gt;What StayPresent &lt;em&gt;does&lt;/em&gt; protect against automatically is path traversal — a request trying to escape the target directory entirely, via &lt;code&gt;../&lt;/code&gt; sequences, absolute paths, or similar tricks. Static asset lookups use &lt;code&gt;send_from_directory&lt;/code&gt; internally, which refuses to serve any path that would resolve outside the intended directory. So while every file &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;templates/&lt;/code&gt; is reachable, nothing &lt;em&gt;outside&lt;/em&gt; it is, regardless of how a request tries to reference it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Time Directory Warning
&lt;/h2&gt;

&lt;p&gt;StayPresent logs a one-time &lt;code&gt;WARNING&lt;/code&gt;-level message through the &lt;code&gt;"staypresent"&lt;/code&gt; logger the first time &lt;code&gt;html()&lt;/code&gt; or &lt;code&gt;markdown()&lt;/code&gt; exposes a given directory as a static-asset fallback — specifically as a reminder that this exposure is directory-wide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ... elsewhere ...
&lt;/span&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;templates/index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# WARNING logged once: directory 'templates/' is now servable as static assets
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This warning fires once per directory per process, not once per request — it's meant to catch your attention during development or the first deploy, not spam your logs continuously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown-Specific Protections: Escaping
&lt;/h2&gt;

&lt;p&gt;Beyond directory exposure, &lt;code&gt;web.markdown()&lt;/code&gt; has its own layer of protection specific to rendering user-authored (or at least file-authored) content into HTML. Plain text — and every recognized Markdown construct — is HTML-escaped before rendering, applied exactly once per character, including inside link/image URLs and titles. A &lt;code&gt;.md&lt;/code&gt; file containing literal &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, or stray &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt; characters, cannot inject markup into the rendered page. Only a fixed list of recognized block-level raw-HTML tags (the kind used for centered logo/badge headers) is ever passed through unescaped — arbitrary inline HTML is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown-Specific Protections: URL Scheme Filtering
&lt;/h2&gt;

&lt;p&gt;Escaping alone doesn't stop an &lt;em&gt;executable&lt;/em&gt; destination — a link that's syntactically valid Markdown but points somewhere dangerous. &lt;code&gt;web.markdown()&lt;/code&gt; checks link/image URLs against a scheme blocklist independently of escaping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;javascript:&lt;/code&gt; and &lt;code&gt;vbscript:&lt;/code&gt; — rejected for both links and images, since these run arbitrary script directly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;file:&lt;/code&gt; — rejected for both, since it enables local filesystem access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data:&lt;/code&gt; — rejected for &lt;strong&gt;links&lt;/strong&gt; specifically (it can smuggle a full HTML document, including script, into a single click), but still allowed for &lt;strong&gt;images&lt;/strong&gt;, since inline &lt;code&gt;data:&lt;/code&gt; images are a common and inert pattern for embedding small icons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rejected URL falls back to plain, already-escaped text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;click me&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;javascript:alert(1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;renders as the plain text &lt;code&gt;click me&lt;/code&gt; — not a working link.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Rejected vs What's Allowed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL type&lt;/th&gt;
&lt;th&gt;Links&lt;/th&gt;
&lt;th&gt;Images&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http(s)://&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relative (&lt;code&gt;/path&lt;/code&gt;, &lt;code&gt;./file&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anchor (&lt;code&gt;#section&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;javascript:&lt;/code&gt; / &lt;code&gt;vbscript:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Structuring Directories Safely
&lt;/h2&gt;

&lt;p&gt;The practical takeaway is directory hygiene: keep the directory passed to &lt;code&gt;web.html()&lt;/code&gt;/&lt;code&gt;web.markdown()&lt;/code&gt; limited to files you're genuinely comfortable being publicly reachable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Safer structure
public/
├── dashboard.html
├── style.css
├── logo.png

secrets/
├── .env
├── credentials.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As long as &lt;code&gt;secrets/&lt;/code&gt; isn't the directory (or a parent of the directory) passed to &lt;code&gt;web.html()&lt;/code&gt;/&lt;code&gt;web.markdown()&lt;/code&gt;, path traversal protection keeps it unreachable regardless of what's inside &lt;code&gt;public/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 'public/' contains only dashboard.html, style.css, and logo.png —
# nothing sensitive lives alongside it.
&lt;/span&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public/dashboard.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat any directory passed to &lt;code&gt;web.html()&lt;/code&gt;/&lt;code&gt;web.markdown()&lt;/code&gt; as fully public — audit its contents the same way you'd audit a directory served by a real static file server.&lt;/li&gt;
&lt;li&gt;Keep secrets, &lt;code&gt;.env&lt;/code&gt; files, and source code in a separate directory tree entirely, never alongside a served template or Markdown file.&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;"staypresent"&lt;/code&gt; logging at &lt;code&gt;WARNING&lt;/code&gt; or above in production specifically so the one-time directory-exposure notice doesn't get lost in &lt;code&gt;INFO&lt;/code&gt;-level noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Putting a template file directly in a project's root directory&lt;/strong&gt;, which then exposes the entire project — source files, config, everything — as static assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming Markdown rendering sanitizes &lt;em&gt;destinations&lt;/em&gt; the same way it escapes &lt;em&gt;text&lt;/em&gt;.&lt;/strong&gt; Escaping and scheme filtering are two separate protections — both matter, and both are handled, but it's worth understanding they're not the same mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relying on "nothing links to it" as a security boundary.&lt;/strong&gt; Every file in the directory is reachable by direct request, whether or not the rendered page itself contains a link to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does this affect &lt;code&gt;web.text()&lt;/code&gt; or &lt;code&gt;web.json()&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No — directory-wide static asset exposure only applies to &lt;code&gt;web.html()&lt;/code&gt; and &lt;code&gt;web.markdown()&lt;/code&gt;, since only those two serve files from disk in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I disable static asset serving entirely?&lt;/strong&gt;&lt;br&gt;
Not currently — it's inherent to how &lt;code&gt;html()&lt;/code&gt;/&lt;code&gt;markdown()&lt;/code&gt; work. The mitigation is directory hygiene, not a configuration flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the URL scheme blocklist apply to &lt;code&gt;web.html()&lt;/code&gt; too?&lt;/strong&gt;&lt;br&gt;
No — that specific protection is part of the Markdown renderer. &lt;code&gt;web.html()&lt;/code&gt; serves whatever HTML you've written as-is, so any sanitization of links/scripts inside a raw &lt;code&gt;.html&lt;/code&gt; file is your own responsibility.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Python static file security&lt;/strong&gt; with StayPresent comes down to two layers: what's protected automatically (path traversal, HTML escaping, dangerous URL schemes in rendered Markdown) and what's still on you (directory contents). Understanding the difference — and keeping served directories limited to genuinely public files — is what keeps a convenient zero-config feature from becoming an accidental information leak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>cybersecurity</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>How StayPresent's Logging Works (Without Breaking Yours)</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:33:42 +0000</pubDate>
      <link>https://dev.to/codenamew/how-staypresents-logging-works-without-breaking-yours-307b</link>
      <guid>https://dev.to/codenamew/how-staypresents-logging-works-without-breaking-yours-307b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A guide to python isolated logging with StayPresent's dedicated logger — no root logger mutation, what gets logged, and how to configure it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How StayPresent's Logging Works (Without Breaking Yours)
&lt;/h2&gt;

&lt;p&gt;A surprisingly common way for a third-party package to quietly break your application's logging is by calling &lt;code&gt;logging.basicConfig()&lt;/code&gt; somewhere in its own code — which mutates the &lt;em&gt;root&lt;/em&gt; logger and can silently change formatting, duplicate output, or override handlers you already configured for your own loggers. StayPresent avoids this entirely through &lt;strong&gt;python isolated logging&lt;/strong&gt;: everything it logs goes through its own dedicated logger, never the root one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The Problem with &lt;code&gt;logging.basicConfig()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;StayPresent's Dedicated Logger&lt;/li&gt;
&lt;li&gt;What Gets Logged, and at What Level&lt;/li&gt;
&lt;li&gt;Adjusting Verbosity&lt;/li&gt;
&lt;li&gt;Attaching Your Own Handler&lt;/li&gt;
&lt;li&gt;Logging During Multi-Bot Runs&lt;/li&gt;
&lt;li&gt;Logging During Shutdown&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Problem with &lt;code&gt;logging.basicConfig()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;logging.basicConfig()&lt;/code&gt; configures the &lt;em&gt;root&lt;/em&gt; logger, which every other logger in your process falls back to unless it's explicitly configured otherwise. If your bot calls it once at startup, and a dependency somewhere else in your stack calls it again, whichever call happens first usually "wins" silently — no error, just unexpected formatting or duplicate log lines that are hard to trace back to their cause. A well-behaved library avoids touching the root logger at all, and instead logs through its own named logger.&lt;/p&gt;

&lt;h2&gt;
  
  
  StayPresent's Dedicated Logger
&lt;/h2&gt;

&lt;p&gt;StayPresent logs exclusively through a logger named &lt;code&gt;"staypresent"&lt;/code&gt;, configured with a single dedicated &lt;code&gt;StreamHandler&lt;/code&gt; and &lt;code&gt;logger.propagate = False&lt;/code&gt;. It never calls &lt;code&gt;logging.basicConfig()&lt;/code&gt;, and it never touches the root logger in any way. This means it cannot clobber, duplicate, or reformat log output your own script has already configured for its own, unrelated loggers — StayPresent's logs and your bot's logs coexist without interfering with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gets Logged, and at What Level
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;"staypresent"&lt;/code&gt; logger reports on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web server startup — whether &lt;code&gt;waitress&lt;/code&gt; or the Flask dev-server fallback is being used, and which host/port it bound to.&lt;/li&gt;
&lt;li&gt;Each bot process starting, crashing, restarting, exhausting its restart budget, or exiting cleanly.&lt;/li&gt;
&lt;li&gt;Signal-triggered shutdowns (&lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt;), including a previously-installed handler being chained afterward, if one exists.&lt;/li&gt;
&lt;li&gt;Unexpected web-server thread failures, both at startup and later during the run.&lt;/li&gt;
&lt;li&gt;Cron pinger start/stop events (at &lt;code&gt;INFO&lt;/code&gt;) and individual failed pings (at &lt;code&gt;WARNING&lt;/code&gt;, via &lt;code&gt;ping()&lt;/code&gt;'s own logging).&lt;/li&gt;
&lt;li&gt;A one-time &lt;code&gt;WARNING&lt;/code&gt; per directory, the first time &lt;code&gt;web.html()&lt;/code&gt;/&lt;code&gt;web.markdown()&lt;/code&gt; exposes it as a static-asset fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adjusting Verbosity
&lt;/h2&gt;

&lt;p&gt;Because it's a normal, named Python logger, you configure it exactly like any other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn it down in a quiet production environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setting only affects StayPresent's own log output — it has no effect on your bot's separate loggers, and vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attaching Your Own Handler
&lt;/h2&gt;

&lt;p&gt;Since it's a real logger object, you can attach additional handlers — to forward StayPresent's events to a file, a monitoring service, or a Discord webhook, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;file_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;file_handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s %(levelname)s %(message)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds to StayPresent's existing dedicated &lt;code&gt;StreamHandler&lt;/code&gt; rather than replacing it, so console output and your new handler both receive events unless you explicitly remove the default one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging During Multi-Bot Runs
&lt;/h2&gt;

&lt;p&gt;When running several bots at once, log lines identify which bot they refer to using a label — normally just the filename (&lt;code&gt;bot[0] 'worker.py'&lt;/code&gt;), or the dotted module path for &lt;code&gt;bot_module&lt;/code&gt;-based bots (&lt;code&gt;bot[0] 'mypkg.bot'&lt;/code&gt;). If two bots share an identical filename, StayPresent automatically switches to logging the full file path for every bot sharing that name, specifically so crash/restart messages stay distinguishable in the logs rather than becoming ambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging During Shutdown
&lt;/h2&gt;

&lt;p&gt;On &lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt;, the shutdown sequence itself is logged — including whether a previously-installed signal handler is being chained afterward. &lt;code&gt;active_cron_handles()&lt;/code&gt; is also checked during shutdown, and any pinger still running at that point is logged purely for visibility (it isn't stopped or waited on, since cron pingers run on daemon threads that exit automatically with the process).&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="c1"&gt;# Turn up StayPresent's own logs for a specific debugging session
&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staypresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Your bot's own logger, configured completely independently
&lt;/span&gt;&lt;span class="n"&gt;bot_logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mybot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bot_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bot_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both loggers coexist cleanly — turning StayPresent's level up or down has zero effect on &lt;code&gt;mybot&lt;/code&gt;'s own output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Turn &lt;code&gt;"staypresent"&lt;/code&gt; up to &lt;code&gt;DEBUG&lt;/code&gt; temporarily when diagnosing a deployment issue (a bot that won't stay up, a crash loop), then back down once resolved.&lt;/li&gt;
&lt;li&gt;Attach a &lt;code&gt;FileHandler&lt;/code&gt; or remote-logging handler to &lt;code&gt;"staypresent"&lt;/code&gt; if you want restart/crash events specifically forwarded somewhere durable, separate from your bot's own application logs.&lt;/li&gt;
&lt;li&gt;Don't call &lt;code&gt;logging.basicConfig()&lt;/code&gt; in your own bot script expecting it to also configure StayPresent's output — it won't, by design; configure &lt;code&gt;"staypresent"&lt;/code&gt; directly instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assuming &lt;code&gt;logging.basicConfig()&lt;/code&gt; in your own code affects StayPresent's logs.&lt;/strong&gt; It doesn't — StayPresent never falls back to root logger configuration, since it never propagates to the root logger at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting the level on the wrong logger.&lt;/strong&gt; &lt;code&gt;logging.getLogger("staypresent")&lt;/code&gt;, not &lt;code&gt;logging.getLogger()&lt;/code&gt; (the root logger) or your own bot's logger name, controls StayPresent's verbosity specifically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expecting bot subprocess output to appear through the &lt;code&gt;"staypresent"&lt;/code&gt; logger.&lt;/strong&gt; Your bot runs as a separate subprocess with its own stdout/stderr — &lt;code&gt;"staypresent"&lt;/code&gt; logs StayPresent's own orchestration events, not your bot's internal application logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does StayPresent ever call &lt;code&gt;logging.basicConfig()&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No, never — this is a deliberate design choice specifically to avoid interfering with logging you've already configured elsewhere in your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I completely silence StayPresent's logs?&lt;/strong&gt;&lt;br&gt;
Yes — set the level above &lt;code&gt;CRITICAL&lt;/code&gt;, or remove/replace its handlers directly if you want full control over its output destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the logging level affect what StayPresent actually does?&lt;/strong&gt;&lt;br&gt;
No — logging is purely observational. Crash recovery, restarts, and shutdown behavior all happen the same way regardless of what level &lt;code&gt;"staypresent"&lt;/code&gt; is set to.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Python isolated logging&lt;/strong&gt; is a small design detail that saves real debugging time — StayPresent's dedicated &lt;code&gt;"staypresent"&lt;/code&gt; logger means you can turn its verbosity up or down, attach your own handlers, and trust that none of it touches the logging you've already built for your bot's own code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>backend</category>
      <category>python</category>
      <category>software</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Graceful Shutdown and Signal Handling in StayPresent</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:32:12 +0000</pubDate>
      <link>https://dev.to/codenamew/graceful-shutdown-and-signal-handling-in-staypresent-7dj</link>
      <guid>https://dev.to/codenamew/graceful-shutdown-and-signal-handling-in-staypresent-7dj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How python graceful shutdown works in StayPresent — SIGINT/SIGTERM handling, install_signal_handlers, and chaining your own signal handler.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Graceful Shutdown and Signal Handling in StayPresent
&lt;/h2&gt;

&lt;p&gt;Deploy platforms and container orchestrators don't just kill your process when they want it to stop — they send a signal first and expect your application to clean up. A bot that ignores &lt;code&gt;SIGTERM&lt;/code&gt; risks corrupted state, orphaned subprocesses, or a database write cut off mid-transaction. This guide covers exactly how &lt;strong&gt;python graceful shutdown&lt;/strong&gt; works inside StayPresent, and how to hook your own cleanup logic into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Signals Matter for Deployment&lt;/li&gt;
&lt;li&gt;What Happens on SIGINT/SIGTERM by Default&lt;/li&gt;
&lt;li&gt;Shutdown During a Restart Backoff&lt;/li&gt;
&lt;li&gt;Chaining Your Own Signal Handler&lt;/li&gt;
&lt;li&gt;Disabling StayPresent's Signal Handling&lt;/li&gt;
&lt;li&gt;Signals and the Main Thread Requirement&lt;/li&gt;
&lt;li&gt;Multi-Bot Shutdown Behavior&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Signals Matter for Deployment
&lt;/h2&gt;

&lt;p&gt;When Render, Railway, Docker, or &lt;code&gt;systemctl stop&lt;/code&gt; needs your process to shut down — for a redeploy, a scale-down event, or a manual stop — the standard mechanism is a &lt;code&gt;SIGTERM&lt;/code&gt; signal, giving the process a window to shut down cleanly before a harder &lt;code&gt;SIGKILL&lt;/code&gt; follows. &lt;code&gt;Ctrl+C&lt;/code&gt; in a terminal sends &lt;code&gt;SIGINT&lt;/code&gt;, the interactive equivalent. Handling both properly is what separates "the bot stopped" from "the bot stopped correctly."&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens on SIGINT/SIGTERM by Default
&lt;/h2&gt;

&lt;p&gt;By default (&lt;code&gt;install_signal_handlers=True&lt;/code&gt;, StayPresent's own default), receiving &lt;code&gt;SIGINT&lt;/code&gt; or &lt;code&gt;SIGTERM&lt;/code&gt; triggers a clean, coordinated teardown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Ctrl+C -&amp;gt; web server and bot process both shut down cleanly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the web server and every running bot subprocess are terminated as part of this sequence — not just one or the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shutdown During a Restart Backoff
&lt;/h2&gt;

&lt;p&gt;There's a subtle race condition this has to handle correctly: what if a signal arrives &lt;em&gt;while&lt;/em&gt; a crashed bot is sitting in its &lt;code&gt;restart_delay&lt;/code&gt; backoff, waiting to respawn? StayPresent synchronizes shutdown and the pending respawn so that exactly one of them wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Either the respawn is cancelled before it happens, or&lt;/li&gt;
&lt;li&gt;Shutdown terminates the freshly-respawned process too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, a shutdown can never leave an untracked, un-terminated bot process running in the background — which is exactly the kind of leak that's easy to introduce with hand-rolled process supervision and hard to notice until your host's process count creeps up over time.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;restart_delay&lt;/code&gt; wait itself is also interruptible — a signal arriving during it wakes the bot's monitor thread immediately, rather than shutdown being held up waiting out the full backoff period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chaining Your Own Signal Handler
&lt;/h2&gt;

&lt;p&gt;If your own script already registers a handler for &lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; calling &lt;code&gt;run()&lt;/code&gt;, StayPresent doesn't silently discard it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Flushing local cache before shutdown...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;my_cleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# On SIGTERM: StayPresent's own cleanup runs first (terminating bot
# processes, logging active cron pingers), THEN my_cleanup runs.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ordering matters: StayPresent's own teardown (bot processes, server, cron pinger visibility) happens first, and your previously-installed handler is called afterward — so your cleanup code can safely assume the bots are already stopped by the time it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disabling StayPresent's Signal Handling
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;install_signal_handlers=False&lt;/code&gt; if your script wants full, exclusive control over shutdown signaling — for example, if you're embedding &lt;code&gt;staypresent.run()&lt;/code&gt; inside a larger application that already has its own signal-handling framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;install_signal_handlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this set, StayPresent won't install any &lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt; handling of its own at all — shutdown behavior becomes entirely whatever your own code implements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signals and the Main Thread Requirement
&lt;/h2&gt;

&lt;p&gt;Python signal handlers can only be registered on the main thread — this is a Python/OS-level constraint, not something StayPresent can work around. If &lt;code&gt;run()&lt;/code&gt; is called from a non-main thread, a warning is logged and graceful signal handling is simply skipped rather than raising an exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# WARNING logged, signal handling skipped
&lt;/span&gt;
&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-Bot Shutdown Behavior
&lt;/h2&gt;

&lt;p&gt;With multiple bots (via &lt;code&gt;bot_file&lt;/code&gt; as a list, &lt;code&gt;bot_module&lt;/code&gt; as a list, or &lt;code&gt;bots&lt;/code&gt;), a &lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt; terminates the web server and &lt;strong&gt;every&lt;/strong&gt; running bot process, not just one. Each bot's own crash-recovery-vs-shutdown race is handled independently and safely, using the same synchronization described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bot(s) shutting down, notifying monitoring...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notify_shutdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notify_shutdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Register your own signal handlers &lt;strong&gt;before&lt;/strong&gt; calling &lt;code&gt;staypresent.run()&lt;/code&gt;, so they get chained rather than needing to be re-registered afterward.&lt;/li&gt;
&lt;li&gt;Keep chained cleanup handlers fast — a slow handler delays how quickly the process actually exits after StayPresent's own teardown finishes.&lt;/li&gt;
&lt;li&gt;Only set &lt;code&gt;install_signal_handlers=False&lt;/code&gt; if you have a concrete reason to manage signals yourself; the default handles the bot-process-and-server coordination correctly out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registering a signal handler after calling &lt;code&gt;run()&lt;/code&gt;.&lt;/strong&gt; StayPresent only chains handlers that were already installed &lt;em&gt;before&lt;/em&gt; &lt;code&gt;run()&lt;/code&gt; starts — anything registered afterward simply replaces StayPresent's own handler rather than being chained to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming &lt;code&gt;install_signal_handlers=False&lt;/code&gt; means "no shutdown happens."&lt;/strong&gt; It just means StayPresent won't install its own signal handling — your own code is then fully responsible for triggering and coordinating a clean shutdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calling &lt;code&gt;run()&lt;/code&gt; from a background thread and being surprised graceful shutdown doesn't work.&lt;/strong&gt; This is a hard Python constraint on where signal handlers can be registered, not a bug — call &lt;code&gt;run()&lt;/code&gt; from the main thread if you need signal handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;SIGKILL&lt;/code&gt; trigger graceful shutdown?&lt;/strong&gt;&lt;br&gt;
No — &lt;code&gt;SIGKILL&lt;/code&gt; can't be caught by any process, StayPresent included. It terminates immediately with no cleanup opportunity at all, which is why platforms typically send &lt;code&gt;SIGTERM&lt;/code&gt; first and only escalate to &lt;code&gt;SIGKILL&lt;/code&gt; after a grace period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to cron() pingers on shutdown?&lt;/strong&gt;&lt;br&gt;
They run on daemon threads and aren't explicitly stopped by &lt;code&gt;run()&lt;/code&gt;'s shutdown sequence — they're simply logged for visibility and then torn down automatically when the process exits, since daemon threads don't block process exit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I trigger the same shutdown sequence programmatically, without an actual OS signal?&lt;/strong&gt;&lt;br&gt;
Not directly — shutdown is driven by the signal handler itself; sending your own process a real &lt;code&gt;SIGTERM&lt;/code&gt; (e.g. via &lt;code&gt;os.kill(os.getpid(), signal.SIGTERM)&lt;/code&gt;) is the supported way to trigger it from within your own code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Python graceful shutdown&lt;/strong&gt; isn't just about catching &lt;code&gt;Ctrl+C&lt;/code&gt; — it's about correctly coordinating a signal against an in-flight restart, terminating every bot process (not just one), and giving your own cleanup code a reliable place to run. StayPresent handles all three by default, while still leaving room to chain your own handler or opt out entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>backend</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>A Deep Dive on StayPresent's ping() and cron() Keep-Warm API</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:06:26 +0000</pubDate>
      <link>https://dev.to/codenamew/a-deep-dive-on-staypresents-ping-and-cron-keep-warm-api-2akp</link>
      <guid>https://dev.to/codenamew/a-deep-dive-on-staypresents-ping-and-cron-keep-warm-api-2akp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Everything about the python self ping scheduler in StayPresent — ping(), cron(), CronHandle, callbacks, and active_cron_handles().&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A Deep Dive on StayPresent's ping() and cron() Keep-Warm API
&lt;/h2&gt;

&lt;p&gt;Most guides on keep-warm pinging stop at "call &lt;code&gt;cron()&lt;/code&gt; every few minutes." That covers the basic case, but StayPresent's &lt;strong&gt;python self ping scheduler&lt;/strong&gt; has more nuance worth understanding — return values, callback safety, &lt;code&gt;CronHandle&lt;/code&gt; lifecycle management, and how it interacts with graceful shutdown. This is the deep-dive reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ping()&lt;/code&gt;: The Synchronous Building Block&lt;/li&gt;
&lt;li&gt;Return Value in Detail&lt;/li&gt;
&lt;li&gt;Host Resolution Rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cron()&lt;/code&gt;: Scheduling Repeated Pings&lt;/li&gt;
&lt;li&gt;Callbacks: &lt;code&gt;on_success&lt;/code&gt; and &lt;code&gt;on_failure&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CronHandle&lt;/code&gt; Object&lt;/li&gt;
&lt;li&gt;Discovering Handles with &lt;code&gt;active_cron_handles()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Interaction with Graceful Shutdown&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ping()&lt;/code&gt;: The Synchronous Building Block
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cron()&lt;/code&gt; is really just &lt;code&gt;ping()&lt;/code&gt; called on a schedule, so understanding &lt;code&gt;ping()&lt;/code&gt; first makes everything else straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;staypresent.ping(host: str, port: int = None, path: str = "/", timeout: float = 10.0, https: bool = None) -&amp;gt; dict&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It sends a single, blocking HTTP GET and returns immediately with the outcome — there's no background thread involved at all for a bare &lt;code&gt;ping()&lt;/code&gt; call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return Value in Detail
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elapsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ok&lt;/code&gt; is &lt;code&gt;True&lt;/code&gt; for any 2xx or 3xx response — not just a bare 200.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;elapsed&lt;/code&gt; is in seconds, rounded to 3 decimal places.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; on success, or a short description on failure (an HTTP error status, a timeout, a DNS failure, connection refused, etc.).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Something&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s wrong:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Host Resolution Rules
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;host&lt;/code&gt; is flexible by design — a bare hostname/IP, a &lt;code&gt;"host:port"&lt;/code&gt;-style string, or a full URL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;host&lt;/code&gt; is a full URL, &lt;code&gt;port&lt;/code&gt;/&lt;code&gt;path&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; are ignored — and if you passed non-default values for any of them anyway, a warning is logged so the mismatch doesn't go unnoticed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"0.0.0.0"&lt;/code&gt; and &lt;code&gt;"::"&lt;/code&gt; are treated as &lt;code&gt;"127.0.0.1"&lt;/code&gt;, since they're bind addresses, not something you can actually send an outgoing request &lt;em&gt;to&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https&lt;/code&gt; defaults to &lt;code&gt;True&lt;/code&gt; for anything that isn't a local address, and &lt;code&gt;False&lt;/code&gt; for &lt;code&gt;"127.0.0.1"&lt;/code&gt;/&lt;code&gt;"localhost"&lt;/code&gt;/&lt;code&gt;"::1"&lt;/code&gt; — matching what &lt;code&gt;staypresent.run()&lt;/code&gt; itself serves locally, so a quick local sanity check (&lt;code&gt;staypresent.ping("127.0.0.1", port=8080)&lt;/code&gt;) works with zero extra configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;cron()&lt;/code&gt;: Scheduling Repeated Pings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;300.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_failure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CronHandle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;host&lt;/code&gt;/&lt;code&gt;port&lt;/code&gt;/&lt;code&gt;path&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; are validated &lt;em&gt;immediately&lt;/em&gt;, at call time — a bad value raises right away rather than only failing silently on the very first scheduled tick, which would otherwise be much harder to notice. &lt;code&gt;interval&lt;/code&gt; must be &lt;code&gt;&amp;gt; 0&lt;/code&gt;. &lt;code&gt;repeat=False&lt;/code&gt; pings exactly once instead of forever, useful for a one-shot delayed warm-up rather than continuous keep-alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Callbacks: &lt;code&gt;on_success&lt;/code&gt; and &lt;code&gt;on_failure&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warm ping ok (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;elapsed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;on_failure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warm ping failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both callbacks receive the same result dict &lt;code&gt;ping()&lt;/code&gt; itself returns. Critically, an exception raised &lt;em&gt;inside&lt;/em&gt; either callback is caught, logged, and swallowed — it can never kill the background pinger thread. This means a buggy &lt;code&gt;on_success&lt;/code&gt; handler degrades to "this particular callback silently didn't do its job" rather than "keep-warm pinging stopped entirely because of an unrelated bug in a logging callback."&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;CronHandle&lt;/code&gt; Object
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cron()&lt;/code&gt; returns a &lt;code&gt;CronHandle&lt;/code&gt; you can use to manage that specific pinger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_running&lt;/span&gt;   &lt;span class="c1"&gt;# True/False
&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;           &lt;span class="c1"&gt;# the URL this cron job pings
&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# stop pinging; safe to call more than once
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stop()&lt;/code&gt; is idempotent — calling it multiple times, or on a handle that's already stopped, doesn't raise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering Handles with &lt;code&gt;active_cron_handles()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Plenty of real code discards &lt;code&gt;cron()&lt;/code&gt;'s return value entirely for a pure fire-and-forget keep-warm pinger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# no assignment at all
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's completely fine — &lt;code&gt;active_cron_handles()&lt;/code&gt; still finds it later, since StayPresent's own registry holds a reference to every handle regardless of whether &lt;em&gt;you&lt;/em&gt; kept one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_cron_handles&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A handle stays in the registry until its &lt;code&gt;.stop()&lt;/code&gt; is called (or its thread otherwise exits) — a discarded return value never causes a keep-warm job to silently vanish from introspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interaction with Graceful Shutdown
&lt;/h2&gt;

&lt;p&gt;Cron pingers run on daemon background threads, so they're never registered with &lt;code&gt;staypresent.run()&lt;/code&gt;'s own shutdown handling the way bot &lt;em&gt;processes&lt;/em&gt; are. In practice this is fine: daemon threads are torn down automatically the moment the process exits, with no cleanup needed. &lt;code&gt;staypresent.run()&lt;/code&gt; does call &lt;code&gt;active_cron_handles()&lt;/code&gt; during its own &lt;code&gt;Ctrl+C&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt; shutdown sequence and logs any pingers still active at that point — purely for visibility, since it neither stops nor waits on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keep-warm ping failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-bot.onrender.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_failure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;log_failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;on_failure&lt;/code&gt; to surface keep-warm ping failures somewhere visible (logs, a monitoring channel) — a silently failing keep-warm pinger looks identical to a healthy one until your service unexpectedly goes to sleep.&lt;/li&gt;
&lt;li&gt;Prefer a bare &lt;code&gt;ping()&lt;/code&gt; call for a one-off manual health check (e.g. in a debugging script), and &lt;code&gt;cron()&lt;/code&gt; only for actual ongoing keep-warm behavior.&lt;/li&gt;
&lt;li&gt;If you need to stop pinging dynamically (e.g. during a maintenance window), keep the returned &lt;code&gt;CronHandle&lt;/code&gt; around and call &lt;code&gt;.stop()&lt;/code&gt; rather than trying to kill the thread yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assuming a callback exception stops the pinger.&lt;/strong&gt; It doesn't — it's caught and logged, which is good for resilience but means a silently broken callback might go unnoticed if you're not checking logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinging &lt;code&gt;"0.0.0.0"&lt;/code&gt; expecting it to reach the public internet.&lt;/strong&gt; It's treated as &lt;code&gt;"127.0.0.1"&lt;/code&gt; — a purely local loopback check, not an external ping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Losing track of a &lt;code&gt;CronHandle&lt;/code&gt; and assuming there's no way to stop it later.&lt;/strong&gt; &lt;code&gt;active_cron_handles()&lt;/code&gt; recovers it even if you never stored the return value yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;cron()&lt;/code&gt; guarantee pings happen exactly every &lt;code&gt;interval&lt;/code&gt; seconds?&lt;/strong&gt;&lt;br&gt;
Pings run one at a time — if a ping takes a while to time out, the gap before the next one grows accordingly rather than piling up parallel requests, so under failure conditions the effective interval can run longer than configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run several &lt;code&gt;cron()&lt;/code&gt; pingers at once?&lt;/strong&gt;&lt;br&gt;
Yes — each call returns its own independent &lt;code&gt;CronHandle&lt;/code&gt;, and &lt;code&gt;active_cron_handles()&lt;/code&gt; returns all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;ping()&lt;/code&gt; block my whole application?&lt;/strong&gt;&lt;br&gt;
Only the calling thread, for up to &lt;code&gt;timeout&lt;/code&gt; seconds. Use &lt;code&gt;cron()&lt;/code&gt; if you need pinging to happen in the background without blocking your main script.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;StayPresent's &lt;strong&gt;python self ping scheduler&lt;/strong&gt; is more than "hit a URL every few minutes" — &lt;code&gt;ping()&lt;/code&gt;'s detailed return value, &lt;code&gt;cron()&lt;/code&gt;'s exception-safe callbacks, and &lt;code&gt;CronHandle&lt;/code&gt;/&lt;code&gt;active_cron_handles()&lt;/code&gt; together give you real visibility and control over keep-warm behavior, instead of a black-box background thread you just have to trust.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>api</category>
      <category>automation</category>
      <category>backend</category>
      <category>python</category>
    </item>
    <item>
      <title>Serving Multiple Endpoints from One Bot with StayPresent</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:01:41 +0000</pubDate>
      <link>https://dev.to/codenamew/serving-multiple-endpoints-from-one-bot-with-staypresent-2le2</link>
      <guid>https://dev.to/codenamew/serving-multiple-endpoints-from-one-bot-with-staypresent-2le2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How to host multiple flask style endpoints for different bots or status pages using StayPresent's path parameter, get_all, and remove.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Serving Multiple Endpoints from One Bot with StayPresent
&lt;/h2&gt;

&lt;p&gt;A single root route is fine for a simple keep-alive check, but as soon as you're running more than one bot, or want a separate status page from your machine-readable health payload, you need &lt;strong&gt;multiple endpoints&lt;/strong&gt; on the same server. StayPresent supports this natively through a &lt;code&gt;path&lt;/code&gt; argument on every response function, without needing to reach for raw Flask routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why One Route Isn't Enough&lt;/li&gt;
&lt;li&gt;Registering Responses at Custom Paths&lt;/li&gt;
&lt;li&gt;Path Normalization Rules&lt;/li&gt;
&lt;li&gt;Trailing Slashes and Static Assets&lt;/li&gt;
&lt;li&gt;Inspecting Every Registered Path&lt;/li&gt;
&lt;li&gt;Removing a Registered Path&lt;/li&gt;
&lt;li&gt;What Happens When Paths Collide&lt;/li&gt;
&lt;li&gt;Full Example: Per-Bot Status Pages&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why One Route Isn't Enough
&lt;/h2&gt;

&lt;p&gt;If you're running two bots via &lt;code&gt;staypresent.run(bots=[...])&lt;/code&gt;, a single JSON blob at &lt;code&gt;/&lt;/code&gt; quickly becomes cramped — you end up manually merging both bots' status into one payload. It's cleaner to give each bot its own endpoint, and keep &lt;code&gt;/&lt;/code&gt; as a simple top-level summary or the default keep-alive response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering Responses at Custom Paths
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;text()&lt;/code&gt;, &lt;code&gt;json()&lt;/code&gt;, &lt;code&gt;html()&lt;/code&gt;, and &lt;code&gt;markdown()&lt;/code&gt; all accept an optional &lt;code&gt;path&lt;/code&gt; argument, defaulting to &lt;code&gt;"/"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;online&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot #2 is alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bot2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dashboard.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHANGELOG.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/changelog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these is served completely independently — a request to &lt;code&gt;/bot2&lt;/code&gt; has no relation to whatever is configured at &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path Normalization Rules
&lt;/h2&gt;

&lt;p&gt;Paths are normalized automatically to avoid subtle, silent bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Surrounding whitespace is stripped — &lt;code&gt;" /status"&lt;/code&gt; and &lt;code&gt;"/status "&lt;/code&gt; (easy to introduce via an f-string) are treated the same as &lt;code&gt;"/status"&lt;/code&gt;, rather than silently registering a route nothing will ever match.&lt;/li&gt;
&lt;li&gt;The path must start with &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Repeated slashes are collapsed.&lt;/li&gt;
&lt;li&gt;A trailing slash is stripped, so &lt;code&gt;"/status/"&lt;/code&gt; and &lt;code&gt;"/status"&lt;/code&gt; refer to the same route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paths must not contain &lt;code&gt;?&lt;/code&gt; or &lt;code&gt;#&lt;/code&gt; — those belong in a query string or fragment, not a route, and StayPresent raises &lt;code&gt;ValueError&lt;/code&gt; if they appear. &lt;code&gt;path&lt;/code&gt; itself must be a non-empty string; anything else raises &lt;code&gt;TypeError&lt;/code&gt;, and an empty or whitespace-only string raises &lt;code&gt;ValueError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trailing Slashes and Static Assets
&lt;/h2&gt;

&lt;p&gt;For &lt;code&gt;html()&lt;/code&gt;/&lt;code&gt;markdown()&lt;/code&gt; registered at any path other than &lt;code&gt;"/"&lt;/code&gt;, a bare request to that path is automatically redirected (HTTP 308) to a trailing-slash version. A request to &lt;code&gt;/dashboard&lt;/code&gt; redirects to &lt;code&gt;/dashboard/&lt;/code&gt;. This is what lets relative asset links inside the file — &lt;code&gt;href="style.css"&lt;/code&gt;, &lt;code&gt;src="images/logo.png"&lt;/code&gt; — resolve against that file's own directory instead of its parent. &lt;code&gt;text()&lt;/code&gt;/&lt;code&gt;json()&lt;/code&gt; responses don't need this since they don't reference any static assets.&lt;/p&gt;

&lt;p&gt;Static asset lookups themselves use the &lt;em&gt;longest matching path prefix&lt;/em&gt; across every registered &lt;code&gt;html()&lt;/code&gt;/&lt;code&gt;markdown()&lt;/code&gt; route, so overlapping mounts resolve unambiguously — a request for &lt;code&gt;/dashboard/style.css&lt;/code&gt; is served from &lt;code&gt;dashboard.html&lt;/code&gt;'s own directory, never mistakenly from the root page's directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspecting Every Registered Path
&lt;/h2&gt;

&lt;p&gt;Three helpers let you introspect what's currently configured, which is useful both for debugging and for building your own tooling on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# ['/', '/bot2', '/changelog', '/dashboard']
&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# {'/': {'type': 'json', 'value': {...}}, '/bot2': {...}, ...}
&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bot2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'type': 'text', 'value': 'bot #2 is alive'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;get()&lt;/code&gt; and &lt;code&gt;get_all()&lt;/code&gt; return an independent deep copy of the current state — mutating the returned dictionary has no effect on what's actually being served. To change the live response, call &lt;code&gt;text()&lt;/code&gt;/&lt;code&gt;json()&lt;/code&gt;/&lt;code&gt;html()&lt;/code&gt;/&lt;code&gt;markdown()&lt;/code&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing a Registered Path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bot2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# True  (it was registered and is now removed)
&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bot2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# False (nothing was registered there)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for a bot that's been shut down or decommissioned but whose status endpoint you don't want to keep serving stale data at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When Paths Collide
&lt;/h2&gt;

&lt;p&gt;Registering the same path twice is completely normal and silent — it's exactly how you update a live response, like calling &lt;code&gt;json()&lt;/code&gt; again with fresh data to refresh a status endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;starting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# ... later ...
&lt;/span&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;# no warning, this is expected usage
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;em&gt;does&lt;/em&gt; get logged is the response &lt;em&gt;type&lt;/em&gt; at a path changing — e.g. a path held &lt;code&gt;"json"&lt;/code&gt; and a later call registers &lt;code&gt;"text"&lt;/code&gt; there instead. That's a much stronger signal that two different call sites weren't aware they were both claiming the same path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;online&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# elsewhere in the code...
&lt;/span&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Online&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# WARNING logged: path '/status' changed from 'json' to 'text'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is blocked either way — the newest registration always wins — this only affects what gets logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example: Per-Bot Status Pages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;online&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/telegram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;online&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/discord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each bot's own status can then be checked independently — &lt;code&gt;/telegram&lt;/code&gt;, &lt;code&gt;/discord&lt;/code&gt;, and the summary at &lt;code&gt;/&lt;/code&gt; — all from a single deployment and port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reserve &lt;code&gt;/&lt;/code&gt; for either the default keep-alive response or a top-level summary, and give each bot its own dedicated &lt;code&gt;path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;web.paths()&lt;/code&gt; during development to sanity-check exactly what's currently registered before deploying.&lt;/li&gt;
&lt;li&gt;If a bot shuts down permanently, call &lt;code&gt;web.remove()&lt;/code&gt; on its path rather than leaving a stale status response behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accidentally reusing a path between two unrelated bots&lt;/strong&gt; without noticing — watch for the &lt;code&gt;WARNING&lt;/code&gt; log line about a response-type change, since that's the signal something's colliding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the trailing-slash redirect for &lt;code&gt;html()&lt;/code&gt;/&lt;code&gt;markdown()&lt;/code&gt;&lt;/strong&gt; and hardcoding a bare path (without the slash) as a link target elsewhere, causing an unnecessary extra redirect hop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building a path with untrimmed whitespace from user input or an f-string&lt;/strong&gt; and assuming it silently fails — it doesn't; normalization handles it, but it's worth understanding why &lt;code&gt;" /status"&lt;/code&gt; and &lt;code&gt;"/status"&lt;/code&gt; behave identically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I register a path other than &lt;code&gt;/health&lt;/code&gt; for health-style checks?&lt;/strong&gt;&lt;br&gt;
Yes — any path is fair game; &lt;code&gt;/health&lt;/code&gt; is only special in that it has a built-in default when nothing else is registered there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does removing a path affect other registered paths?&lt;/strong&gt;&lt;br&gt;
No — &lt;code&gt;web.remove()&lt;/code&gt; only affects the specific path you pass; everything else stays exactly as configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a limit to how many paths I can register?&lt;/strong&gt;&lt;br&gt;
No documented limit — register as many independent responses as your deployment needs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Serving &lt;strong&gt;multiple endpoints from one bot&lt;/strong&gt; — or from several bots sharing one deployment — doesn't require reaching for raw Flask routes. StayPresent's &lt;code&gt;path&lt;/code&gt; argument, combined with &lt;code&gt;get_all()&lt;/code&gt;, &lt;code&gt;paths()&lt;/code&gt;, and &lt;code&gt;remove()&lt;/code&gt;, gives you a small but complete routing layer without leaving the &lt;code&gt;staypresent.web&lt;/code&gt; module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>api</category>
      <category>backend</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Fixing "Attempted Relative Import" Errors with StayPresent's bot_module</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 02:59:08 +0000</pubDate>
      <link>https://dev.to/codenamew/fixing-attempted-relative-import-errors-with-staypresents-botmodule-47g8</link>
      <guid>https://dev.to/codenamew/fixing-attempted-relative-import-errors-with-staypresents-botmodule-47g8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How to fix the python attempted relative import error when deploying a packaged bot, using StayPresent's bot_module parameter instead of bot_file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fixing "Attempted Relative Import" Errors with StayPresent's bot_module
&lt;/h2&gt;

&lt;p&gt;If your bot is structured as a proper Python package — with relative imports like &lt;code&gt;from . import handlers&lt;/code&gt; scattered across its modules — you've probably hit this the moment you tried to run it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportError: attempted relative import with no known parent package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the most common &lt;strong&gt;attempted relative import&lt;/strong&gt; errors developers run into when deploying a packaged bot, and it has nothing to do with your bot's logic — it's purely about &lt;em&gt;how&lt;/em&gt; the script gets launched. This guide covers why it happens and how StayPresent's &lt;code&gt;bot_module&lt;/code&gt; parameter fixes it without restructuring your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why the Error Happens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python file.py&lt;/code&gt; vs &lt;code&gt;python -m module&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;bot_module&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Multiple Module-Based Bots&lt;/li&gt;
&lt;li&gt;Mixing File-Based and Module-Based Bots&lt;/li&gt;
&lt;li&gt;What's Different From &lt;code&gt;bot_file&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why the Error Happens
&lt;/h2&gt;

&lt;p&gt;Python treats a script run directly (&lt;code&gt;python bot.py&lt;/code&gt;) as a top-level module with no package context — it has no &lt;code&gt;__package__&lt;/code&gt;, so any &lt;code&gt;from . import something&lt;/code&gt; inside it (or inside anything it imports) fails immediately, because there's no "parent package" for the dot to refer to. This has nothing to do with your file structure being wrong; it's purely a consequence of &lt;em&gt;how&lt;/em&gt; the interpreter was invoked.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;python file.py&lt;/code&gt; vs &lt;code&gt;python -m module&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;python mypkg/bot.py&lt;/code&gt; executes the file as a standalone script. Running &lt;code&gt;python -m mypkg.bot&lt;/code&gt; instead executes it &lt;em&gt;as a module inside its package&lt;/em&gt;, with &lt;code&gt;__package__&lt;/code&gt; set correctly — which is exactly what relative imports need to resolve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── mypkg/
│   ├── __init__.py
│   ├── bot.py        # uses `from . import handlers`
│   └── handlers.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python mypkg/bot.py       &lt;span class="c"&gt;# ImportError: attempted relative import&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; mypkg.bot       &lt;span class="c"&gt;# works correctly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using &lt;code&gt;bot_module&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;staypresent.run()&lt;/code&gt;'s &lt;code&gt;bot_file&lt;/code&gt; parameter runs your bot the same way &lt;code&gt;python &amp;lt;file&amp;gt;&lt;/code&gt; would — which means it inherits this exact limitation. &lt;code&gt;bot_module&lt;/code&gt; runs it the way &lt;code&gt;python -m &amp;lt;module&amp;gt;&lt;/code&gt; would instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot_module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mypkg.bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No changes to your bot's own code are needed — the fix is entirely in how StayPresent launches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Module-Based Bots
&lt;/h2&gt;

&lt;p&gt;Just like &lt;code&gt;bot_file&lt;/code&gt;, &lt;code&gt;bot_module&lt;/code&gt; accepts a list for multiple bots, each independently supervised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot_module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mypkg.bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mypkg.worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bot_args&lt;/code&gt;/&lt;code&gt;env&lt;/code&gt; apply identically to every module in the list, exactly as they would with &lt;code&gt;bot_file&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixing File-Based and Module-Based Bots
&lt;/h2&gt;

&lt;p&gt;Inside the &lt;code&gt;bots&lt;/code&gt; parameter, use &lt;code&gt;"module"&lt;/code&gt; instead of &lt;code&gt;"file"&lt;/code&gt; per entry — and you can freely combine both styles in the same call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--verbose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord_bot.worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each entry must set exactly one of &lt;code&gt;"file"&lt;/code&gt;/&lt;code&gt;"module"&lt;/code&gt; — setting both, or neither, raises &lt;code&gt;TypeError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Different From &lt;code&gt;bot_file&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A few important differences to be aware of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No existence check up front.&lt;/strong&gt; A file path can be checked for existence before anything starts; a module path genuinely can't be verified as importable without actually importing it — which would mean running its parent packages' &lt;code&gt;__init__.py&lt;/code&gt; as a side effect inside the orchestrating process, something StayPresent deliberately avoids. A missing or misspelled module isn't caught until that bot subprocess actually starts and Python itself reports &lt;code&gt;No module named '...'&lt;/code&gt; — which is then handled through the normal crash/restart logic like any other failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working directory matters.&lt;/strong&gt; The subprocess is launched from the same working directory as your orchestrating script. &lt;code&gt;bot_module="mypkg.bot"&lt;/code&gt; needs to be resolvable from wherever you actually run your &lt;code&gt;main.py&lt;/code&gt;, exactly as if you'd typed &lt;code&gt;python -m mypkg.bot&lt;/code&gt; from that same location yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log labels use the dotted module path.&lt;/strong&gt; Instead of a filename, log lines show something like &lt;code&gt;bot[0] 'mypkg.bot'&lt;/code&gt;. If two bots share the exact same module string, there's no further-qualifying fallback the way there is for file paths (no "directory" to disambiguate with) — those log lines are distinguished only by their &lt;code&gt;bot[i]&lt;/code&gt; index.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bot_module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mypkg.bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;restart_on_crash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your bot already runs correctly locally via &lt;code&gt;python -m mypkg.bot&lt;/code&gt;, use &lt;code&gt;bot_module&lt;/code&gt; in &lt;code&gt;staypresent.run()&lt;/code&gt; rather than trying to work around relative imports with &lt;code&gt;sys.path&lt;/code&gt; hacks.&lt;/li&gt;
&lt;li&gt;Keep your deployment's working directory consistent between local testing and production — if &lt;code&gt;python -m mypkg.bot&lt;/code&gt; only works from a specific directory locally, that same constraint applies to your hosting platform's start command.&lt;/li&gt;
&lt;li&gt;Since module paths aren't validated up front, watch your logs closely on the very first deploy of a module-based bot to catch a typo'd module path quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passing a file path to &lt;code&gt;bot_module&lt;/code&gt;&lt;/strong&gt; (e.g. &lt;code&gt;bot_module="mypkg/bot.py"&lt;/code&gt;) instead of a dotted module path (&lt;code&gt;"mypkg.bot"&lt;/code&gt;) — this will fail the same way running &lt;code&gt;python -m mypkg/bot.py&lt;/code&gt; would from the command line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming &lt;code&gt;bot_module&lt;/code&gt; validates the module exists before starting&lt;/strong&gt;, and being surprised when a typo only surfaces as a crash-and-restart cycle rather than an immediate, upfront error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running the orchestrating script from the wrong directory&lt;/strong&gt;, so &lt;code&gt;mypkg.bot&lt;/code&gt; can't be found even though the code itself is correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need to change my bot's code to use &lt;code&gt;bot_module&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No — if your bot already works when launched with &lt;code&gt;python -m&lt;/code&gt;, no code changes are needed at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I mix &lt;code&gt;bot_file&lt;/code&gt; and &lt;code&gt;bot_module&lt;/code&gt; at the top level of &lt;code&gt;run()&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No, they're mutually exclusive at the top level — use the &lt;code&gt;bots&lt;/code&gt; list if you need both styles in the same deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn't StayPresent just validate the module up front like it does for files?&lt;/strong&gt;&lt;br&gt;
Verifying a module is importable requires actually importing it, which means executing its package's &lt;code&gt;__init__.py&lt;/code&gt; as a side effect — something StayPresent avoids doing inside the process that's supposed to just be orchestrating, not running, your bot's code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;attempted relative import error&lt;/strong&gt; is one of the most common surprises when deploying a properly packaged bot. &lt;code&gt;bot_module&lt;/code&gt; solves it by launching your bot exactly the way &lt;code&gt;python -m&lt;/code&gt; would, with no restructuring of your project and no &lt;code&gt;sys.path&lt;/code&gt; workarounds required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>programming</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Turn Your README Into a Live Website with StayPresent's Markdown Renderer</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 02:57:06 +0000</pubDate>
      <link>https://dev.to/codenamew/turn-your-readme-into-a-live-website-with-staypresents-markdown-renderer-5bm2</link>
      <guid>https://dev.to/codenamew/turn-your-readme-into-a-live-website-with-staypresents-markdown-renderer-5bm2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How to serve a markdown to html python page — README, changelog, or docs — with zero dependencies using StayPresent's built-in renderer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Turn Your README Into a Live Website with StayPresent's Markdown Renderer
&lt;/h2&gt;

&lt;p&gt;Most bots already have a &lt;code&gt;README.md&lt;/code&gt; or &lt;code&gt;CHANGELOG.md&lt;/code&gt; sitting in the repo, fully written and maintained, but invisible to anyone who isn't looking at the source code. StayPresent's &lt;code&gt;web.markdown()&lt;/code&gt; turns any &lt;strong&gt;markdown to html python&lt;/strong&gt; file into a styled, GitHub-like web page — served directly from your bot's own HTTP server, with zero extra dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Serve Markdown Directly&lt;/li&gt;
&lt;li&gt;Basic Usage&lt;/li&gt;
&lt;li&gt;What the Renderer Supports&lt;/li&gt;
&lt;li&gt;Light, Dark, and Auto Theming&lt;/li&gt;
&lt;li&gt;Favicon, Title, and Description&lt;/li&gt;
&lt;li&gt;Security: Escaping and URL Filtering&lt;/li&gt;
&lt;li&gt;What It Doesn't Support&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Serve Markdown Directly
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;CHANGELOG.md&lt;/code&gt; is genuinely useful to end users and other developers — but only if they can actually read it without cloning the repo. Since StayPresent's HTTP server already exists for keep-alive purposes, serving that same Markdown file as a proper web page costs nothing extra and requires no separate documentation site or static site generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHANGELOG.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like &lt;code&gt;html()&lt;/code&gt;, the file is read and re-rendered fresh on every request — editing &lt;code&gt;CHANGELOG.md&lt;/code&gt; on disk is reflected the next time someone loads the page, with no restart required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;favicon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file_path&lt;/code&gt; must exist at call time — a typo raises &lt;code&gt;FileNotFoundError&lt;/code&gt; immediately rather than only 404ing once a browser requests the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Renderer Supports
&lt;/h2&gt;

&lt;p&gt;Everything is handled by StayPresent's own built-in renderer — &lt;strong&gt;no &lt;code&gt;markdown&lt;/code&gt; package or any other dependency is required.&lt;/strong&gt; It covers the constructs people actually use in real READMEs and docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ATX and Setext headings (levels 1–6), each with a stable, GitHub-compatible anchor &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Bold, italic, bold-italic, and strikethrough.&lt;/li&gt;
&lt;li&gt;Inline code spans, including multi-backtick spans.&lt;/li&gt;
&lt;li&gt;Links, images, and bare autolinks — including URLs with a single level of balanced parentheses.&lt;/li&gt;
&lt;li&gt;Unordered and ordered lists, including nesting, multi-paragraph items, tight/loose rendering, custom start numbers, and GitHub-style task-list checkboxes (&lt;code&gt;- [ ]&lt;/code&gt; / &lt;code&gt;- [x]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Nested blockquotes.&lt;/li&gt;
&lt;li&gt;Fenced and indented code blocks, with language-tag syntax highlighting hooks.&lt;/li&gt;
&lt;li&gt;GitHub-flavored pipe tables, including per-column alignment.&lt;/li&gt;
&lt;li&gt;Horizontal rules and hard line breaks.&lt;/li&gt;
&lt;li&gt;Raw HTML passthrough for recognized block-level tags — which is what makes a centered logo/badge header at the top of a README render correctly.&lt;/li&gt;
&lt;li&gt;Backslash escapes for literal punctuation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nested inline content — a code span or bold text inside a link, or an image inside a link (the common "clickable badge" pattern) — is fully supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;![badge&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;badge.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;](https://example.com)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;renders as a proper clickable image link, not broken markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Light, Dark, and Auto Theming
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mode&lt;/code&gt; controls how the rendered page handles color scheme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guide.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mode="auto"&lt;/code&gt; (default): the page includes &lt;code&gt;&amp;lt;meta name="color-scheme" content="light dark"&amp;gt;&lt;/code&gt; with no forced theme — the visitor's own OS/browser preference decides.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode="light"&lt;/code&gt; / &lt;code&gt;mode="dark"&lt;/code&gt;: forces that theme for every visitor via a &lt;code&gt;data-theme&lt;/code&gt; attribute, regardless of their own system setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;mode&lt;/code&gt; is case-insensitive and whitespace-tolerant (&lt;code&gt;" Dark "&lt;/code&gt; works fine); anything outside &lt;code&gt;"light"&lt;/code&gt;/&lt;code&gt;"dark"&lt;/code&gt;/&lt;code&gt;"auto"&lt;/code&gt;/&lt;code&gt;None&lt;/code&gt; raises &lt;code&gt;ValueError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Favicon, Title, and Description
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs/guide.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;favicon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;favicon.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Project Docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Everything you need to get started.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;favicon&lt;/code&gt; accepts either a direct URL (used as-is) or a relative path next to &lt;code&gt;file_path&lt;/code&gt; — resolved the same way neighboring static assets already are, and checked for existence immediately, just like &lt;code&gt;file_path&lt;/code&gt; itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; sets both &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and an &lt;code&gt;og:title&lt;/code&gt; Open Graph tag, falling back to the Markdown file's own filename if omitted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt; adds a &lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt; tag plus &lt;code&gt;og:description&lt;/code&gt; — useful for link-preview cards when the page gets shared in a chat app or on social media.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security: Escaping and URL Filtering
&lt;/h2&gt;

&lt;p&gt;Plain text and every recognized Markdown construct is HTML-escaped before rendering, so a &lt;code&gt;.md&lt;/code&gt; file containing &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags or stray &lt;code&gt;&amp;lt;&lt;/code&gt;/&lt;code&gt;&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;amp;&lt;/code&gt; characters can't inject markup into the page — only a fixed list of recognized block-level raw-HTML tags is ever passed through unescaped.&lt;/p&gt;

&lt;p&gt;Link and image destinations are additionally checked against a scheme blocklist, independent of the escaping above: &lt;code&gt;javascript:&lt;/code&gt; and &lt;code&gt;vbscript:&lt;/code&gt; are rejected outright for both links and images, &lt;code&gt;file:&lt;/code&gt; is rejected for both, and &lt;code&gt;data:&lt;/code&gt; is rejected for links specifically (while still permitted for images, since inline &lt;code&gt;data:&lt;/code&gt; images are a common, inert pattern). A rejected URL simply falls back to plain, escaped text rather than becoming a working link.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Neighboring files are exposed the same way &lt;code&gt;html()&lt;/code&gt; exposes them.&lt;/strong&gt; Everything in &lt;code&gt;file_path&lt;/code&gt;'s directory becomes servable as a static asset — not only the files actually referenced from the page. Keep the Markdown file's directory limited to files you're comfortable being publicly reachable; StayPresent logs a one-time &lt;code&gt;WARNING&lt;/code&gt; the first time a directory gets exposed this way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What It Doesn't Support
&lt;/h2&gt;

&lt;p&gt;This is a lightweight renderer, not a full CommonMark implementation. It intentionally does not support: footnotes, definition lists as distinct syntax, custom containers/admonitions, LaTeX/math, reference-style links (&lt;code&gt;[text][ref]&lt;/code&gt; + a separate &lt;code&gt;[ref]: url&lt;/code&gt; line), or blockquote lazy continuation. For any of these, pre-render your Markdown with a different tool and serve the resulting HTML with &lt;code&gt;web.html()&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My Bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A Telegram bot for X, Y, and Z.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHANGELOG.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/changelog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;.md&lt;/code&gt; files intended for public serving in a directory that contains only files you're fine exposing — the whole directory becomes reachable, not just the file you called &lt;code&gt;markdown()&lt;/code&gt; on.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;mode="auto"&lt;/code&gt; unless you have a specific brand reason to force a theme — it respects the visitor's own preference with zero extra effort.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;title&lt;/code&gt;/&lt;code&gt;description&lt;/code&gt; for any page you expect to get linked or shared, so previews render sensibly instead of falling back to a bare filename.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expecting reference-style links (&lt;code&gt;[text][ref]&lt;/code&gt;) to work.&lt;/strong&gt; They don't — only the inline &lt;code&gt;[text](url)&lt;/code&gt; form is supported; rewrite reference-style links before serving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placing sensitive files in the same directory as a publicly served &lt;code&gt;.md&lt;/code&gt; file.&lt;/strong&gt; They become reachable via the same static-asset mechanism, regardless of whether anything links to them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming a relative &lt;code&gt;favicon&lt;/code&gt; resolves against the current working directory.&lt;/strong&gt; It resolves relative to &lt;code&gt;file_path&lt;/code&gt;'s own directory, not the process's cwd.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Turning &lt;strong&gt;markdown to html&lt;/strong&gt; in Python doesn't require pulling in a templating engine or a separate documentation build step. &lt;code&gt;staypresent.web.markdown()&lt;/code&gt; renders your existing &lt;code&gt;README.md&lt;/code&gt; or &lt;code&gt;CHANGELOG.md&lt;/code&gt; into a clean, GitHub-styled page — complete with theming, favicon, and link-preview metadata — using nothing beyond what StayPresent already ships with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>documentation</category>
      <category>markdown</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Multiple Bots in One Process with StayPresent</title>
      <dc:creator>John Wick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 02:53:01 +0000</pubDate>
      <link>https://dev.to/codenamew/multiple-bots-in-one-process-with-staypresent-3lj0</link>
      <guid>https://dev.to/codenamew/multiple-bots-in-one-process-with-staypresent-3lj0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How to run multiple python bots in a single process using StayPresent's bot_file list and bots parameter, with independent crash recovery for each.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Running Multiple Bots in One Process with StayPresent
&lt;/h2&gt;

&lt;p&gt;If you're managing a Telegram bot pipeline with several workers, or a Discord bot that runs alongside a separate polling worker, deploying each one as its own hosting service quickly gets expensive and repetitive. StayPresent's multi-bot support lets you &lt;strong&gt;run multiple python bots&lt;/strong&gt; from a single &lt;code&gt;staypresent.run()&lt;/code&gt; call, each fully and independently supervised, while sharing one HTTP server and one deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Run Multiple Bots Together&lt;/li&gt;
&lt;li&gt;The Simple Way: A List of Files&lt;/li&gt;
&lt;li&gt;Per-Bot Configuration with &lt;code&gt;bots&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How Independent Supervision Works&lt;/li&gt;
&lt;li&gt;Shared Failures vs Independent Failures&lt;/li&gt;
&lt;li&gt;Log Labels When Filenames Collide&lt;/li&gt;
&lt;li&gt;Full Example&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Common Mistakes&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Run Multiple Bots Together
&lt;/h2&gt;

&lt;p&gt;A common real-world setup is a Telegram ingestion bot and a separate upload/processing worker that share a queue or database — logically two processes, but deployed together because they belong to the same service. Running each as a separate hosting deployment means twice the port-binding boilerplate, twice the health checks, and twice the deployment overhead for something that's conceptually one unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simple Way: A List of Files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;bot_file&lt;/code&gt; accepts either a single string or a list of strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both scripts launch as independent subprocesses, sharing the same &lt;code&gt;bot_args&lt;/code&gt;/&lt;code&gt;env&lt;/code&gt; if provided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-Bot Configuration with &lt;code&gt;bots&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When each bot needs different arguments or environment variables, use &lt;code&gt;bots&lt;/code&gt; — a list of dicts, one per bot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--verbose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bots&lt;/code&gt; is mutually exclusive with &lt;code&gt;bot_file&lt;/code&gt;/&lt;code&gt;bot_args&lt;/code&gt;/&lt;code&gt;env&lt;/code&gt; — passing both raises &lt;code&gt;TypeError&lt;/code&gt;, so there's no ambiguity about which configuration style is active. Each entry requires a &lt;code&gt;"file"&lt;/code&gt; key; &lt;code&gt;"args"&lt;/code&gt; and &lt;code&gt;"env"&lt;/code&gt; are optional per entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Independent Supervision Works
&lt;/h2&gt;

&lt;p&gt;Every bot in the list gets its own restart counter and its own dedicated monitoring thread. A crash in one bot — and any subsequent restart attempts — has zero effect on the others. If your Telegram bot hits an unhandled exception and restarts three times, your Discord bot keeps running the entire time, completely unaffected.&lt;/p&gt;

&lt;p&gt;Before anything starts, every bot file listed is validated to exist — a missing file raises &lt;code&gt;FileNotFoundError&lt;/code&gt; immediately, before any process launches. And if the very first launch attempt of &lt;em&gt;any&lt;/em&gt; bot fails outright (a bad interpreter path, the system running out of file descriptors), StayPresent terminates any bots that already started and raises the failure immediately, so you never end up with orphaned, untracked processes left running in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Failures vs Independent Failures
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;staypresent.run()&lt;/code&gt; waits for every bot to finish — or fail permanently — before returning or exiting the process. If a bot exhausts its &lt;code&gt;max_restarts&lt;/code&gt; budget, it's marked permanently failed, but the &lt;em&gt;other&lt;/em&gt; bots keep running normally. Only once every bot has finished does StayPresent decide the process's own exit code: non-zero if any bot ended in a permanently-failed state, and specifically the exit code of the lowest-indexed failing bot if more than one failed — deterministic, regardless of which bot's monitor thread happens to finish first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;optional_worker.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;optional_worker.py&lt;/code&gt; permanently fails but &lt;code&gt;critical_bot.py&lt;/code&gt; keeps running fine, the whole process only exits (with &lt;code&gt;optional_worker.py&lt;/code&gt;'s exit code) once &lt;code&gt;critical_bot.py&lt;/code&gt; itself eventually finishes too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log Labels When Filenames Collide
&lt;/h2&gt;

&lt;p&gt;Bots are tracked by their position in the list, not by filename — two bots can share the exact same filename (&lt;code&gt;shard_a/bot.py&lt;/code&gt; and &lt;code&gt;shard_b/bot.py&lt;/code&gt;) with no functional conflict. But identical filenames in the logs would make crash/restart messages ambiguous, so StayPresent detects the collision automatically and logs each bot's full file path instead of just the filename in that case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bot[0] 'shard_a/bot.py' crashed, restarting...
bot[1] 'shard_b/bot.py' crashed, restarting...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bots with non-colliding filenames still get the shorter, plain filename label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;staypresent&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;staypresent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--verbose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord_bot.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue_worker.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;max_restarts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;restart_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;bots&lt;/code&gt; (not &lt;code&gt;bot_file&lt;/code&gt; + shared &lt;code&gt;env&lt;/code&gt;) whenever different processes genuinely need different configuration — it keeps the config next to the file it applies to instead of implicitly shared.&lt;/li&gt;
&lt;li&gt;Give each bot its own status endpoint with &lt;code&gt;web.text()&lt;/code&gt;/&lt;code&gt;web.json()&lt;/code&gt; at a distinct &lt;code&gt;path&lt;/code&gt; so you can check any individual bot's state without guessing which one a shared root response refers to.&lt;/li&gt;
&lt;li&gt;Keep a "critical" bot and "optional" bots in mind when tuning &lt;code&gt;max_restarts&lt;/code&gt; — a bot you can't afford to lose deserves a higher budget than a background helper.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixing &lt;code&gt;bot_file&lt;/code&gt; and &lt;code&gt;bots&lt;/code&gt; in the same call.&lt;/strong&gt; This raises &lt;code&gt;TypeError&lt;/code&gt; immediately — pick one style per &lt;code&gt;run()&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming one bot's crash stops the others.&lt;/strong&gt; It doesn't — each bot is supervised entirely independently, which is often surprising the first time you see it in the logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting that &lt;code&gt;run()&lt;/code&gt; only returns once every bot is done.&lt;/strong&gt; If one bot runs forever and another exits cleanly, the process is still alive waiting on the first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I mix &lt;code&gt;bot_file&lt;/code&gt;-style and &lt;code&gt;bot_module&lt;/code&gt;-style bots?&lt;/strong&gt;&lt;br&gt;
Yes, inside &lt;code&gt;bots&lt;/code&gt; — set &lt;code&gt;"file"&lt;/code&gt; for some entries and &lt;code&gt;"module"&lt;/code&gt; for others, freely mixed in the same list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does each bot get its own restart budget?&lt;/strong&gt;&lt;br&gt;
Yes — &lt;code&gt;max_restarts&lt;/code&gt;, &lt;code&gt;restart_delay&lt;/code&gt;, and &lt;code&gt;restart_reset_after&lt;/code&gt; apply per bot, not shared across the whole group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I only need one bot?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;bot_file="bot.py"&lt;/code&gt; still works exactly as before — multi-bot support doesn't change single-bot usage at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Whether you're pairing a Telegram ingestion bot with a processing worker, or running several unrelated scripts side by side, StayPresent lets you &lt;strong&gt;run multiple python bots&lt;/strong&gt; from one deployment, one port, and one &lt;code&gt;run()&lt;/code&gt; call — with each bot's crashes, restarts, and logs kept fully independent of the others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;staypresent[prod]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>automation</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
