<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Snehangshu's Blog]]></title><description><![CDATA[I write about tech that is fun, with occasionally complex topics on Cloud, DevOps, Web Development and Telecommunications]]></description><link>https://blogs.snehangshu.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 12:08:46 GMT</lastBuildDate><atom:link href="https://blogs.snehangshu.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A Step-by-Step Guide to Advanced Task Scheduling using Celery Beat and Django]]></title><description><![CDATA[In my asynchronous Python blog, I explored multitasking, distributed computing, and related best practices. But sometimes we need to run programs in a periodic manner, where running tasks in parallel does not matter. In many applications, periodic ta...]]></description><link>https://blogs.snehangshu.dev/a-step-by-step-guide-to-advanced-task-scheduling-using-celery-beat-and-django</link><guid isPermaLink="true">https://blogs.snehangshu.dev/a-step-by-step-guide-to-advanced-task-scheduling-using-celery-beat-and-django</guid><category><![CDATA[celery]]></category><category><![CDATA[cronjob]]></category><category><![CDATA[scheduled task]]></category><category><![CDATA[Django]]></category><category><![CDATA[Celery Beat]]></category><category><![CDATA[System Design]]></category><category><![CDATA[django admin]]></category><category><![CDATA[Python]]></category><category><![CDATA[Redis]]></category><category><![CDATA[rabbitmq]]></category><dc:creator><![CDATA[Snehangshu Bhattacharya]]></dc:creator><pubDate>Tue, 12 Nov 2024 17:57:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731433818410/434ef8b1-34a3-4679-a326-ed40dc350a3c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my asynchronous Python <a target="_blank" href="https://blogs.snehangshu.dev/python-multitasking-key-practices-and-challenges">blog</a>, I explored multitasking, distributed computing, and related best practices. But sometimes we need to run programs in a periodic manner, where running tasks in parallel does not matter. In many applications, periodic tasks are used to handle backup, cleanup, maintenance, update, batch processing etc. Periodic tasks are essential to maintain system efficiency and reliability.</p>
<p>In Unix-like operating systems (Linux, BSD, Mac OS) these periodic tasks are called as <code>Cron jobs</code>. For Windows, we call them <code>Scheduled Tasks</code>.</p>
<p>In this article, I will briefly discuss cron jobs, how the syntax works and its limitations. Next, I will jump into the celery beat scheduler and it works. Then, I’ll discuss how we can use Django (specifically Django Admin) to control the scheduled tasks easily. Finally, we will see how to use the celery flower plugin to visualize submitted tasks and execution results.</p>
<p>This blog assumes the reader has some working knowledge of Celery, the distributed task queue. If you want to quickly implement a task scheduler for your application, this article can be a quick start. If you are unaware of Celery, it is recommended that you read my previous blog: <a target="_blank" href="https://blogs.snehangshu.dev/distributed-computing-with-python-unleashing-the-power-of-celery-for-scalable-applications">Distributed Computing with Python: Unleashing the Power of Celery for Scalable Applications</a>.</p>
<h1 id="heading-cron-jobs">Cron Jobs🔧</h1>
<h2 id="heading-how-to">How to</h2>
<p>Cron jobs are tasks scheduled to run at specified intervals. Cron is available in Unix-based operating systems like Linux and Mac OS.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⏰</div>
<div data-node-type="callout-text">The word <code>Cron</code> is not a mispronunciation of Corn (🌽). Cron jobs are named after Chronos, the Greek god of time!</div>
</div>

<p>Cron jobs are executed by a background process called <code>cron daemon</code>. It checks the cron table (or crontab) for scheduled commands and executes them according to the timing information associated. The syntax of a crontab is five time-and-date fields followed by the command to execute. For example -</p>
<pre><code class="lang-bash">* * * * * /<span class="hljs-built_in">command</span>/to/execute
</code></pre>
<p>Each time fields from left to right are-</p>
<ol>
<li><p>Minute (0-59)</p>
</li>
<li><p>Hour (0-23)</p>
</li>
<li><p>Day of the month (1-31)</p>
</li>
<li><p>Month (1-12)</p>
</li>
<li><p>Day of week (0-7, 0 and 7 both represent Sunday)</p>
</li>
</ol>
<p>Let us see some examples of how crontab entries can be interpreted -</p>
<pre><code class="lang-bash"><span class="hljs-comment"># At 22:00 on every day-of-week from Monday through Friday</span>
0 22 * * 1-5 /<span class="hljs-built_in">command</span>/to/execute

<span class="hljs-comment"># At 00:05 in August</span>
5 0 * 8 * /<span class="hljs-built_in">command</span>/to/execute

<span class="hljs-comment"># At 04:05 on Sunday</span>
5 4 * * sun /<span class="hljs-built_in">command</span>/to/execute

<span class="hljs-comment"># At every 5th minute</span>
*/5 * * * * /<span class="hljs-built_in">command</span>/to/execute
</code></pre>
<p>To edit a <code>crontab</code> you need to run -</p>
<pre><code class="lang-bash"><span class="hljs-comment"># To edit the current user crontab</span>
crontab -e
<span class="hljs-comment"># To edit the root user crontab</span>
sudo crontab -e
</code></pre>
<h2 id="heading-use-cases">Use cases</h2>
<ol>
<li><p><strong>Backups:</strong> Database and file system backups are essential to any application to protect it from data loss. Backup jobs are scheduled during off-peak hours with cron jobs.</p>
</li>
<li><p><strong>System Updates:</strong> With crontabs, we can check for system updates in intervals ensuring security and performance of the systems.</p>
</li>
<li><p><strong>Health Checks:</strong> Task monitoring and health checks can be done periodically to check on server health/performance using cron jobs.</p>
</li>
<li><p><strong>Log Rotation:</strong> Compression, backup and deletion of logs are essential to save server storage space. These operations can be automated using cron jobs.</p>
</li>
<li><p><strong>Email Automation:</strong> We can send automated emails or alerts using cron jobs.</p>
</li>
<li><p>and many more…</p>
</li>
</ol>
<h2 id="heading-limitations">Limitations</h2>
<ol>
<li><p><strong>Minimum Interval:</strong> The minimum interval supported by cron is 1 minute (defined by <code>*/1 * * * *</code> or <code>* * * * *</code>). Cron can not run any jobs that require more granular intervals.</p>
</li>
<li><p><strong>No logging:</strong> Cron does not output any logs, so troubleshooting cron jobs can be difficult unless you set up logging with your task.</p>
</li>
<li><p><strong>No error handling:</strong> Cron does not retry your failed jobs, it also does not report or retry them. If you want your errors handled, you need to go DIY.</p>
</li>
<li><p><strong>Bad resource utilization:</strong> Cron can hog system resources if the job is too heavy or has some bugs, and slow down the server hindering normal operations.</p>
</li>
<li><p><strong>Task Overlap:</strong> If a job takes a long time to execute, it can overlap with the same job for the next run. This can have unexpected results in program logic and also can be a resource hog.</p>
</li>
<li><p><strong>No support for complex or non-periodic schedules:</strong> Cron has no built-in support for jobs like <code>Run every first Monday of a month at 5 AM</code> or <code>Run every other week at Tuesday midnight</code>. To implement these complex schedules, you need to find a workaround or add checks inside the job to do so.</p>
</li>
<li><p><strong>Limited Environment:</strong> Cron runs in a minimal shell environment that does not have all the environment variables set like the user’s interactive shell. Commands that run perfectly in the user’s shell might fail due to this in a cron job.</p>
</li>
</ol>
<h1 id="heading-celery-beat">Celery Beat🥬</h1>
<p>In my last <a target="_blank" href="https://blogs.snehangshu.dev/distributed-computing-with-python-unleashing-the-power-of-celery-for-scalable-applications#heading-getting-task-results-with-redis-backend">article</a>, I have discussed celery in depth. To schedule a task in a celery we usually call <code>&lt;task&gt;.delay()</code> or <code>&lt;task&gt;.apply_async()</code>. However, these methods have to be run by your program logic and do not fulfil the criteria for running periodic tasks.</p>
<p><strong>Celery Beat</strong> is a scheduler that enqueues tasks based on a schedule that are executed by available celery worker nodes.</p>
<h2 id="heading-run-beat">Run Beat</h2>
<p>For our basic setup, create a Python environment (my choice of environment manager is <code>miniconda</code>) and install the following dependencies -</p>
<p><code>requirements.txt</code></p>
<pre><code class="lang-bash">celery==5.4.0
redis==5.2.0
</code></pre>
<pre><code class="lang-bash">pip install -r requirements.txt
</code></pre>
<p>Next, run the following docker-compose file to launch the celery backend (rabbitmq) and broker (redis):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">rabbitmq:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">rabbitmq:latest</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5672:5672"</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:latest</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6379:6379"</span>
</code></pre>
<pre><code class="lang-bash">docker compose up -d
</code></pre>
<p>Next, define the <strong>Celery</strong> app in <code>app.py</code> -</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery <span class="hljs-keyword">import</span> Celery

app = Celery(<span class="hljs-string">"beat-worker"</span>, 
             backend=<span class="hljs-string">"redis://localhost:6379/0"</span>,
             broker=<span class="hljs-string">"amqp://guest@localhost:5672//"</span>)
</code></pre>
<p>Finally, run the <strong>Celery Beat Scheduler</strong> -</p>
<pre><code class="lang-bash">$ celery -A app beat -l INFO
__    -    ... __   -        _
LocalTime -&gt; 2024-11-05 22:26:38
Configuration -&gt;
    . broker -&gt; amqp://guest:**@localhost:5672//
    . loader -&gt; celery.loaders.app.AppLoader
    . scheduler -&gt; celery.beat.PersistentScheduler
    . db -&gt; celerybeat-schedule
    . logfile -&gt; [stderr]@%INFO
    . maxinterval -&gt; 5.00 minutes (300s)
[2024-11-05 22:26:38,868: INFO/MainProcess] beat: Starting...
</code></pre>
<p>After running beat, you might notice that it does not do anything. That’s because we have not created a task and created a schedule for the beat scheduler. Let’s define a task and a schedule.</p>
<p>Create a new file called <code>tasks.py</code> in the same folder, this is where we will define our tasks.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> app <span class="hljs-keyword">import</span> app
<span class="hljs-keyword">import</span> subprocess

<span class="hljs-meta">@app.task()</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log_uptime</span>(<span class="hljs-params">logfile_name: str</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    subprocess.run(<span class="hljs-string">f"uptime &gt;&gt; <span class="hljs-subst">{logfile_name}</span>"</span>, shell=<span class="hljs-literal">True</span>)
</code></pre>
<p>We just declared our first task, which will log any system’s uptime, the number of logged-in users, and the average load of the system in a file. When run periodically, it will append the output to the given filename.</p>
<p>Next, we also need to include the task in the Celery configuration and define a schedule so that it can run periodically. To do this, modify <code>app.py</code> like this -</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery <span class="hljs-keyword">import</span> Celery
<span class="hljs-keyword">import</span> random

app = Celery(<span class="hljs-string">"beat-worker"</span>,
             backend=<span class="hljs-string">"redis://localhost:6379/0"</span>,
             broker=<span class="hljs-string">"amqp://guest@localhost:5672//"</span>,
             include=<span class="hljs-string">"tasks"</span>) <span class="hljs-comment"># include the tasks file where our tasks are</span>
app.conf.beat_schedule = {
    <span class="hljs-string">'log-uptime'</span>: {
        <span class="hljs-string">'task'</span>: <span class="hljs-string">'tasks.log_uptime'</span>, <span class="hljs-comment"># define our task method</span>
        <span class="hljs-string">'schedule'</span>: <span class="hljs-number">10.0</span>, <span class="hljs-comment"># define our interval, 10 second</span>
        <span class="hljs-string">'args'</span>: (<span class="hljs-string">'uptime.log'</span>,) <span class="hljs-comment"># pass arguments to our task, if any</span>
    }
}
</code></pre>
<p>Next, restart the celery beat and spin up a worker node, in separate shells -</p>
<pre><code class="lang-bash">$ celery -A app beat -l INFO
</code></pre>
<iframe src="https://asciinema.org/a/qM2JpKa20anqWTlj29qoX221l/iframe?size=normal&amp;speed=1&amp;autoplay=false&amp;loop=false&amp;theme=asciinema&amp;t=0&amp;startAt=0&amp;preload=true&amp;cols=90&amp;rows=20&amp;i=true&amp;idleTimeLimit=2" id="asciicast-iframe-" style="overflow:hidden;margin:0px;border:0px;display:inline-block;width:100%;float:none;visibility:visible;height:480px"></iframe>

<pre><code class="lang-bash">$ celery -A worker beat -l INFO
</code></pre>
<iframe src="https://asciinema.org/a/np3H5crLHD3BVbGqMxZr8Run3/iframe?size=normal&amp;speed=1&amp;autoplay=false&amp;loop=false&amp;theme=asciinema&amp;t=0&amp;startAt=0&amp;preload=true&amp;cols=90&amp;rows=20&amp;i=true&amp;idleTimeLimit=2" id="asciicast-iframe-" style="overflow:hidden;margin:0px;border:0px;display:inline-block;width:100%;float:none;visibility:visible;height:480px"></iframe>

<p>The beat schedules the task every 10 seconds and the worker node executes it. After several runs, the <code>uptime.log</code> looks like this:</p>
<pre><code class="lang-plaintext">12:26  up 9 days, 19:50, 2 users, load averages: 2.27 2.20 2.14
12:26  up 9 days, 19:50, 2 users, load averages: 2.15 2.18 2.13
12:27  up 9 days, 19:50, 2 users, load averages: 2.20 2.19 2.14
12:27  up 9 days, 19:51, 2 users, load averages: 2.03 2.15 2.13
21:32  up 11 days,  4:55, 2 users, load averages: 3.44 3.24 3.77
21:32  up 11 days,  4:56, 2 users, load averages: 3.23 3.20 3.74
21:32  up 11 days,  4:56, 2 users, load averages: 2.82 3.11 3.71
21:32  up 11 days,  4:56, 2 users, load averages: 2.62 3.06 3.68
</code></pre>
<p>Now that we have established the basics, let us discuss how to configure different celery schedules.</p>
<h2 id="heading-celery-schedules">Celery Schedules</h2>
<p>By default, Celery offers three different types of schedules -</p>
<ol>
<li><p>Interval</p>
</li>
<li><p>Crontab</p>
</li>
<li><p>Solar</p>
</li>
</ol>
<h3 id="heading-interval">Interval</h3>
<p>This is the simplest type of schedule available, a demo of this schedule is already shown in the above example. It takes seconds as interval input which is a float. So unlike <code>Cron Jobs</code>, setting up sub-minute resolution tasks is possible with this type of schedule.</p>
<h3 id="heading-crontab">Crontab</h3>
<p>Celery beat supports crontab-style schedule expressions too. If we want to run our uptime logger every Monday at 3 AM, the crontab expression will be <code>0 3 * * mon</code>. To use this as a Celery schedule -</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery.schedules <span class="hljs-keyword">import</span> crontab

app.conf.beat_schedule = {
    <span class="hljs-string">'log-uptime'</span>: {
        <span class="hljs-string">'task'</span>: <span class="hljs-string">'tasks.log_uptime'</span>, <span class="hljs-comment"># define our task method</span>
        <span class="hljs-string">'schedule'</span>: crontab(minute=<span class="hljs-string">'0'</span>, hour=<span class="hljs-string">'3'</span>, day_of_month=<span class="hljs-string">'*'</span>, month_of_year=<span class="hljs-string">'*'</span>, day_of_week=<span class="hljs-string">'mon'</span>),
        <span class="hljs-string">'args'</span>: (<span class="hljs-string">'uptime.log'</span>,) <span class="hljs-comment"># pass arguments to our task, if any</span>
    }
}
</code></pre>
<p>While vanilla Celery does not have any way to parse the whole cron string, it should be very easy to just split the Cron text and feed the individual expressions into the <code>crontab</code> class.</p>
<pre><code class="lang-python">cron_str = <span class="hljs-string">'0 3 * * mon'</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">parse_crontab</span>(<span class="hljs-params">cron_str: str</span>):</span>
    minute, hour, day_of_month, month_of_year, day_of_week = cron_str.split()
    <span class="hljs-keyword">return</span> crontab(
        minute=minute,
        hour=hour,
        day_of_month=day_of_month,
        month_of_year=month_of_year,
        day_of_week=day_of_week
    )
</code></pre>
<h3 id="heading-solar">Solar</h3>
<p><img src="https://media.istockphoto.com/id/1095165938/vector/illustration-of-a-clock-with-the-time-of-day-and-am-pm-flat-des.jpg?s=612x612&amp;w=0&amp;k=20&amp;c=uAaYBg9ki7ZEv2qAvt7wfe4mjWSOAMXhpi7V5mgsdhE=" alt="12,600+ Day Cycle Stock Illustrations, Royalty-Free Vector Graphics &amp; Clip  Art - iStock | Sunrise, Waking up, Time lapse" class="image--center mx-auto" /></p>
<p>Solar schedules are a unique way of scheduling tasks based on location coordinates, and key solar events like dawn, sunrise, dusk, sunset etc. These schedules are useful in applications like -</p>
<ol>
<li><p>Smart Home and Smart Street Lights</p>
</li>
<li><p>Agricultural Automation</p>
</li>
<li><p>Renewable Energy Monitoring</p>
</li>
<li><p>Wildlife Tracking</p>
</li>
<li><p>Environmental Data Collection</p>
</li>
</ol>
<p>My home city is <code>Kolkata</code>, and its co-ordinate is <code>22.5744° N, 88.3629° E</code>. If I want to schedule a task that triggers at <code>sunrise</code> in Kolkata, there is no crontab or interval way to do this. We can use the solar schedule here like this-</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery.schedules <span class="hljs-keyword">import</span> solar

app.conf.beat_schedule = {
    <span class="hljs-string">'log-uptime'</span>: {
        <span class="hljs-string">'task'</span>: <span class="hljs-string">'tasks.log_uptime'</span>, <span class="hljs-comment"># define our task method</span>
        <span class="hljs-string">'schedule'</span>: solar(<span class="hljs-string">'sunrise'</span>, <span class="hljs-number">22.5744</span>, <span class="hljs-number">88.3629</span>),
        <span class="hljs-string">'args'</span>: (<span class="hljs-string">'uptime.log'</span>,) <span class="hljs-comment"># pass arguments to our task, if any</span>
    }
}
</code></pre>
<p>You can find a list of available solar events here: <a target="_blank" href="https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html#solar-schedules">https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html#solar-schedules</a></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: Solar schedules depend on precise astronomical calculations. These calculations are done using <a target="_self" href="https://pypi.org/project/ephem/"><code>ephem</code></a> library.</div>
</div>

<p>To use solar schedules, we need to include the <code>ephem</code> library in our <code>requirements.txt</code>:</p>
<pre><code class="lang-plaintext">celery==5.4.0
redis==5.2.0
ephem==4.1.6
</code></pre>
<h1 id="heading-database-backed-tasks-with-django">Database-backed Tasks with Django💽</h1>
<p>Django is an open-source Python web framework that follows the Model-View-Template(MVT) architecture. It is one of the leading Python web frameworks offering security, modularity, and reusability in one package. Built-in Object Relation Mapper (ORM), authentication and an admin interface make it easy to start writing your logic while Django handles the boilerplate.</p>
<p>In this section, we will set up a Django project, configure Celery with it and use the <code>django-celery-beat</code> library to create Celery schedules with a GUI. Also, we will discuss some schedule types that are offered by the library.</p>
<h2 id="heading-setup-a-django-project">Setup a Django Project</h2>
<p>As discussing Django in depth is not the goal of this article, I will show a very basic setup of Django that serves our purpose. If you are experienced in Django, you can skip to the setup of <code>django-celery-beat</code>.</p>
<p>To start, add Django to your <code>requirements.txt</code> and install it -</p>
<pre><code class="lang-plaintext">celery==5.4.0
redis==5.2.0
ephem==4.1.6
Django==5.1.3
</code></pre>
<p>Create a new Django project named <code>beat_it</code> -</p>
<pre><code class="lang-bash">django-admin startproject beat_it
</code></pre>
<p>Django creates all of the necessary files in this structure -</p>
<pre><code class="lang-plaintext">beat_it
├── beat_it
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
└── manage.py
</code></pre>
<p>By default Django uses <code>sqlite3</code> as database backend, which will be stored in a local file. for the first time, we need to initialize the database by performing a migration. Go inside the project directory and run -</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> beat_it
python manage.py migrate
</code></pre>
<p>Django Admin automates the creation of admin sites for the defined models which removes the need to develop any admin panel. All models registered to Django Admin support CRUD operations through GUI. As we are done setting up the database, we need to create a superuser to access the Django admin dashboard. Fill up the username, email and password as instructed to create a superuser.</p>
<pre><code class="lang-bash">python manage.py createsuperuser
</code></pre>
<p>Next, we will run the development server and access the admin panel -</p>
<pre><code class="lang-bash">python manage.py runserver
</code></pre>
<p>Open <a target="_blank" href="http://127.0.0.1:8000">http://127.0.0.1:8000</a> in your browser, and you will see the welcome page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731403778423/8c15013c-24bc-449e-8fe6-22e8e2966b17.png" alt class="image--center mx-auto" /></p>
<p>To access the admin panel, open <a target="_blank" href="http://127.0.0.1:8000/admin">http://127.0.0.1:8000/admin</a> and log in with your superuser credentials. You will see a page like this -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731404213426/cea4858b-e41a-4e84-bf12-a77056fad268.png" alt class="image--center mx-auto" /></p>
<p>Next, create an app <code>demoapp</code> inside our project where we will define our tasks -</p>
<pre><code class="lang-bash">python manage.py startapp demoapp
</code></pre>
<p>Congratulations, you have successfully set up your Django project. Next, we will set up the <code>django-celery-beat</code> library.</p>
<h2 id="heading-set-up-django-celery-beat">Set up <code>django-celery-beat</code></h2>
<p><code>django-celery-beat</code> is a library that uses Django ORM to save schedules and tasks in the database, adds some handy schedule types, and allows us to manage all of these things from <code>Django Admin</code>, making it an almost no-code solution.</p>
<p>To get started, include it in <code>requirements.txt</code> and install -</p>
<pre><code class="lang-plaintext">django-celery-beat==2.7.0
</code></pre>
<p>We will first configure Celery, but this time we will do it by defining the backend and broker in Django <code>settings.py</code> and later importing those settings while initializing Celery.</p>
<p>In <code>beat_it/beat_it/settings.py</code>, inside <code>INSTALLED_APPS</code> array add our created app <code>demoapp</code>, and <code>django_celery_beat</code> and add <code>CELERY_BROKER_URL</code>, <code>CELERY_RESULT_BACKEND</code>, and <code>CELERY_BEAT_SCHEDULER</code> lines at the end of the file -</p>
<pre><code class="lang-python">INSTALLED_APPS = [
    ...,
    <span class="hljs-string">'demoapp'</span>,
    <span class="hljs-string">'django_celery_beat'</span>,
]
...
CELERY_BROKER_URL = <span class="hljs-string">'amqp://guest@localhost:5672//'</span>
CELERY_RESULT_BACKEND = <span class="hljs-string">'redis://localhost:6379/0'</span>
CELERY_BEAT_SCHEDULER = <span class="hljs-string">'django_celery_beat.schedulers.DatabaseScheduler'</span>
</code></pre>
<p>Next, create a new <code>celery.py</code> in <code>beat_it/beat_it</code> path of our project. The file is explained below -</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> celery <span class="hljs-keyword">import</span> Celery

<span class="hljs-comment"># Set the location of django settings in environment</span>
os.environ.setdefault(<span class="hljs-string">"DJANGO_SETTINGS_MODULE"</span>, <span class="hljs-string">"beat_it.settings"</span>)

app = Celery(<span class="hljs-string">'beat_it'</span>) <span class="hljs-comment"># Initialize celery</span>
<span class="hljs-comment"># import celery config from settings.py, use namespace prefix 'CELERY'</span>
<span class="hljs-comment"># This is how BROKER_URL, RESULT_BACKEND, BEAT_SCHEDULER variables </span>
<span class="hljs-comment"># are imported</span>
app.config_from_object(<span class="hljs-string">"django.conf:settings"</span>, namespace=<span class="hljs-string">"CELERY"</span>)
<span class="hljs-comment"># Automatically discover tasks defined in a 'tasks.py' present</span>
<span class="hljs-comment"># in the modules from the INSTALLED_APPS array</span>
app.autodiscover_tasks()
</code></pre>
<p><code>django-celery-beat</code> defines some models that are stored in the database and are possible to edit in <code>Django Admin</code>. We need to run migrations once more to sync the database and create the tables for those models.</p>
<pre><code class="lang-bash">python manage.py migrate django_celery_beat
</code></pre>
<p>The <code>Django Admin</code> dashboard will look like this after the migrations -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731413113391/609972d6-1eac-4482-a31d-9d8224e79edb.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-setting-up-schedules">Setting up Schedules</h2>
<p><code>django-celery-beat</code> offers some useful wrappers for existing celery schedules and more. It has the following schedules supported -</p>
<ol>
<li><p><strong>Clocked Schedule:</strong> Useful for defining a schedule for tasks that run once at a fixed date and time. We can create one with code -</p>
<pre><code class="lang-python"> <span class="hljs-keyword">from</span> django_celery_beat.models <span class="hljs-keyword">import</span> ClockedSchedule
 <span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime, timedelta

 <span class="hljs-comment"># A schedule that will run once after 10 minutes from now</span>
 time_after_10_min = datetime.now() + timedelta(minutes=<span class="hljs-number">10</span>)
 ClockedSchedule.objects.create(clocked_time=time_after_10_min)
</code></pre>
<p> This same operation can also be done with <code>Django Admin</code> panel. To create, visit <a target="_blank" href="http://127.0.0.1:8000/admin/django_celery_beat/clockedschedule/add/">http://127.0.0.1:8000/admin/django_celery_beat/clockedschedule/add/</a> and fill up the date and time.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731415237793/bad3e9a4-b92c-4c76-bee6-ad6dddb7ad1a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Crontab Schedule:</strong> This schedule is very similar to vanilla Celery crontabs discussed before, with the added feature of Timezone awareness. To create a crontab schedule for expression <code>0 3 * * mon</code> for Indian Standard Time (IST) zone -</p>
<pre><code class="lang-python"> <span class="hljs-keyword">from</span> django_celery_beat.models <span class="hljs-keyword">import</span> CrontabSchedule
 <span class="hljs-keyword">import</span> zoneinfo <span class="hljs-comment"># for setting the timezone</span>

 CrontabSchedule.objects.create(
     minute=<span class="hljs-string">'0'</span>,
     hour=<span class="hljs-string">'3'</span>,
     day_of_week=<span class="hljs-string">'mon'</span>,
     day_of_month=<span class="hljs-string">'*'</span>,
     month_of_year=<span class="hljs-string">'*'</span>,
     timezone=zoneinfo.ZoneInfo(<span class="hljs-string">'Asia/Kolkata'</span>)
 )
</code></pre>
<p> Similarly in <code>Django Admin</code> UI -</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731416147734/7e249e19-90da-4512-aba6-d097cce8850c.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Interval Schedule:</strong> This one is a very useful wrapper around the basic interval scheduler that Celery implements. While the basic Celery scheduler offers an interval setting only in seconds, this one has resolution options of <code>days</code>, <code>hours</code>, <code>minutes</code>, <code>seconds</code>, and <code>microseconds</code>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731416833949/f5045ec7-900d-4c56-8cd4-3a7e0b4859f5.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Solar Schedule:</strong> Solar Schedule is also a wrapper around the Celery implementation. You can set the <code>Latitude</code>, <code>Longitude</code> and <code>Solar Event</code> type like sunrise, sunset, dawn, dusk etc.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731417074229/49e7db7d-0d42-4df5-92c3-c262b314c13f.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-setting-up-tasks">Setting up Tasks</h2>
<p>Now we can define some tasks in <code>beat_it/demoapp/tasks.py</code>. We can define our same uptime logger script here -</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery <span class="hljs-keyword">import</span> shared_task
<span class="hljs-keyword">import</span> subprocess

<span class="hljs-meta">@shared_task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log_uptime</span>(<span class="hljs-params">logfile_name: str</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    subprocess.run(<span class="hljs-string">f"uptime &gt;&gt; <span class="hljs-subst">{logfile_name}</span>"</span>, shell=<span class="hljs-literal">True</span>)
</code></pre>
<p>As we have set up task auto-discovery, this task will be already visible to the Celery worker when we run it.</p>
<p>To add this task to <code>django-celery-beat</code> go to <code>Django Admin</code> and add a <code>Periodic Task</code>: <a target="_blank" href="http://127.0.0.1:8000/admin/django_celery_beat/periodictask/add/">http://127.0.0.1:8000/admin/django_celery_beat/periodictask/add/</a></p>
<p>Add a <code>Name</code>, under Task (custom) write the full module path of the task, e.g. <code>demoapp.tasks.log_uptime</code>, keep <code>Enabled</code> checked and select a schedule of any type that you have created before, I chose an interval of <code>every 5 seconds</code>. If the task has any argument, pass it on to the <code>Arguments</code> or <code>Keyword Arguments</code> section. Leave other settings as default and click save.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731422926971/bff01b0b-663f-4246-872c-6c3f20668c43.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731422959322/d1d7c86d-1ed5-47f2-aee1-839a6198f342.png" alt class="image--center mx-auto" /></p>
<p>Our task is now set up and ready to run!</p>
<h2 id="heading-running-tasks">Running Tasks</h2>
<p>Running a Celery worker cluster does not change much with <code>django-celery-beat</code>. The same Celery worker concurrency models that I discussed in my previous <a target="_blank" href="https://blogs.snehangshu.dev/distributed-computing-with-python-unleashing-the-power-of-celery-for-scalable-applications#heading-celery-worker-concurrency-models">blog</a> apply here as well! To keep things simple, we will go with the default prefork model and run the Celery worker -</p>
<pre><code class="lang-bash">celery -A beat_it worker -l INFO
</code></pre>
<p>As our worker node is ready to accept tasks, we now need to launch the <code>Celery Beat</code> scheduler -</p>
<pre><code class="lang-bash">celery -A beat_it beat -l INFO
</code></pre>
<p>Beat will start enqueuing the tasks as defined in the database according to the schedule.</p>
<h1 id="heading-cluster-monitoring-with-celery-flower">Cluster Monitoring with Celery Flower 🌸</h1>
<p>Flower is an open-source web interface to monitor and manage Celery clusters. It can show real-time information on celery workers, tasks, and brokers.</p>
<h2 id="heading-installation">Installation</h2>
<p>Flower is available from <a target="_blank" href="https://pypi.org/project/flower/">pypi.org</a>, run -</p>
<pre><code class="lang-bash">pip install flower==2.0.1
</code></pre>
<h2 id="heading-run-flower">Run Flower</h2>
<p>From our <code>beat_it</code> project directory, run -</p>
<pre><code class="lang-bash">celery -A beat_it flower -l INFO
</code></pre>
<p>Celery Flower will be running at <a target="_blank" href="http://0.0.0.0:5555">http://0.0.0.0:5555</a>.</p>
<h2 id="heading-monitoring">Monitoring</h2>
<p>Flower displays the number of active, processed, failed, succeeded, and retried events for workers on the home page. You can find detailed information about tasks, their results, errors, runtime, and much more on the Flower dashboard. I'll leave the exploration of it up to you!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731425665754/6c0dbc1f-e3f7-4df4-bfd0-0a04517cdf93.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-its-a-wrap">It’s a Wrap!🎁</h1>
<p>In conclusion, Celery Beat with Django offers a robust solution for managing periodic tasks in applications. While traditional cron jobs have their limitations, such as minimum intervals and lack of error handling, Celery Beat tries to address these issues with precise job schedules, logging via Celery backend, unique schedules etc.</p>
<p>By integrating Celery with Django, developers can leverage the <code>Django Admin</code> interface to easily manage and schedule tasks, reducing the boilerplate code and introducing dynamic adjustments. Additionally, using tools like Celery Flower for monitoring provides valuable insights into task execution, error tracking, and system performance.</p>
<p>This combination of technologies empowers developers to implement complex scheduling requirements and maintain optimal resource utilization, making it an ideal choice for modern applications.</p>
<p>Please find the GitHub repo with all the code here: <a target="_blank" href="https://github.com/f0rkb0mbZ/celery-beat-demo.git">https://github.com/f0rkb0mbZ/celery-beat-demo.git</a></p>
<p>It takes a lot of time and effort to write lengthy blogs with demos like this. If you've made it this far and appreciate my effort, please give it a 💜. You can also share this blog with anyone who might find it useful.</p>
<h1 id="heading-references">References 📝</h1>
<ol>
<li><p><a target="_blank" href="https://docs.celeryq.dev/en/latest/userguide/periodic-tasks.html"><strong>Celery Beat Docs</strong></a></p>
</li>
<li><p><a target="_blank" href="https://django-celery-beat.readthedocs.io/en/latest/">Django-celery-beat Docs</a></p>
</li>
<li><p><a target="_blank" href="https://flower.readthedocs.io/en/latest/">Celery Flower Docs</a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Distributed Computing with Python: Unleashing the Power of Celery for Scalable Applications]]></title><description><![CDATA[Have you ever wondered about when you upload an 8K Ultra HD video to YouTube and immediately YouTube starts to process and optimize that video and make multiple copies of it in 1080p, 720p, 480p, 360p and 144p so that your content can be streamed to ...]]></description><link>https://blogs.snehangshu.dev/distributed-computing-with-python-unleashing-the-power-of-celery-for-scalable-applications</link><guid isPermaLink="true">https://blogs.snehangshu.dev/distributed-computing-with-python-unleashing-the-power-of-celery-for-scalable-applications</guid><category><![CDATA[Python]]></category><category><![CDATA[celery]]></category><category><![CDATA[distributed system]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[Task queue]]></category><category><![CDATA[rabbitmq]]></category><category><![CDATA[FFmpeg]]></category><category><![CDATA[Redis]]></category><category><![CDATA[scalability]]></category><category><![CDATA[video streaming]]></category><category><![CDATA[message broker]]></category><category><![CDATA[multiprocessing]]></category><category><![CDATA[Scalable web applications]]></category><category><![CDATA[Computer Science]]></category><category><![CDATA[computer networking]]></category><dc:creator><![CDATA[Snehangshu Bhattacharya]]></dc:creator><pubDate>Sun, 06 Oct 2024 08:01:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728198273197/d1ea6019-9a59-44a4-b603-528321a386ea.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever wondered about when you upload an 8K Ultra HD video to YouTube and immediately YouTube starts to process and optimize that video and make multiple copies of it in 1080p, 720p, 480p, 360p and 144p so that your content can be streamed to every network and every device of every size in the world? YouTube does this for not only your video but also at least 1 hour of video content is uploaded to YouTube every second! The answer is distributed systems and job queues. Distributed systems are an extremely important part of system design and are used by top IT companies around the world to serve billions of users!</p>
<p>In this blog, you will learn how to build scalable distributed Python applications capable of handling millions of tasks a minute. This is the second part of my two-part blog series on Multitasking with Python. You can read the previous part <a target="_blank" href="https://blogs.snehangshu.dev/python-multitasking-key-practices-and-challenges">here</a>.</p>
<p>First, I will explain task queues and how they work. Then, we'll explore message brokers to build a solid foundation. Once we've covered the basics, we'll dive into Celery in detail and finally create a project to transcode videos in bulk using Celery. Get ready; this is going to be a long blog!</p>
<h2 id="heading-task-queues">Task Queues🏭</h2>
<p>Before we discuss Celery's architecture, we need to establish what task queues are and how they help achieve computing in a distributed manner. In the last part, we discussed techniques for getting more work from a single multi-core, multi-thread system. However, task queues can span multiple systems, even data centres, and thus offer excellent horizontal scalability, which is only limited by hardware, cost, and more physical constraints. Some of the key concepts of task queues are listed below:</p>
<ol>
<li><p><strong>Task:</strong> Tasks are individual units of work (usually a function or method) placed in the queue to be processed. They are usually compute-heavy or I/O-heavy and take some time to complete.</p>
</li>
<li><p><strong>Producers and Consumers:</strong> Producers are parts of an application that creates tasks and add them to the task queue. Consumers of workers are applications that pick up tasks from the task queue and execute them. Producers and consumers run independently of each other, and this level of decoupling adds to the scalable nature of task queues. There can be multiple producers and consumers in a Task Queue. Producers and consumers can be interchangeably named as <code>publishers</code> and <code>subscribers</code>, and this programming model is known as the pub/sub model.</p>
</li>
<li><p><strong>Broker:</strong> Producers and consumers are connected via task queues. The physical implementation of task queues is provided by message brokers. Brokers act as a communication highway between producers and consumers. Usually, the queues implemented by brokers are First-In-First-Out by nature, but we can assign priority to tasks to control the execution order.</p>
</li>
</ol>
<p>Some common use cases of task queues include background job processing, data pipelines, long-running asynchronous processes etc. Some examples of task queues include Celery (obviously), Sidekiq (written in Ruby, used in the backend of GitLab), RQ or Redis Queue (Redis-based job queue in Python), etc. Here is a list of the most commonly used task queues: <a target="_blank" href="https://taskqueues.com/#libraries">https://taskqueues.com/#libraries</a>.</p>
<h2 id="heading-message-brokers-and-rabbitmq">Message Brokers and RabbitMQ</h2>
<h3 id="heading-a-6-lane-highway-for-pubsub-model">A 6-lane highway 🛣️ for pub/sub model</h3>
<p>If <code>Task</code> is the unit of work for a Task Queue, the unit of communication for a message broker is <code>Message</code>. The message can be in <code>JSON</code>, <code>XML</code>, binary, or any other serialised format and can include events, complete functions, commands etc. A message broker is a piece of software that enables communication between <code>producers</code> and <code>consumers</code>, and is the cornerstone of this decoupled architecture where both <code>producers</code> and <code>consumers</code> can work and scale independently.</p>
<p>Before we discuss <code>Celery</code>, we should discuss RabbitMQ, the most used, fast and resource-efficient message broker for <code>Celery</code>.</p>
<h3 id="heading-rabbitmq-one-broker-to-queue-them-all">RabbitMQ: One broker to queue them all🐰</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602696511/08fcd75d-b4ca-4ada-a016-a3dd59ac2965.png" alt class="image--center mx-auto" /></p>
<p><code>RabbitMQ</code> is an open-source message broker written in <code>Erlang</code>💡 that allows communication between different applications by routing messages. It uses widely adopted <strong>AMQP (A</strong>dvanced <strong>M</strong>essage <strong>Q</strong>ueueing <strong>P</strong>rotocol) to communicate between systems. RabbitMQ also supports <strong>MQTT (M</strong>essage <strong>Q</strong>ueuing <strong>T</strong>elemetry <strong>T</strong>ransport, heavily used in IoT applications), and <strong>STOMP (S</strong>imple/<strong>S</strong>treaming <strong>T</strong>ext <strong>O</strong>rientated <strong>M</strong>essaging <strong>P</strong>rotocol) protocols to interact with other systems.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Ericsson developed Erlang in 1986 to handle millions of phone calls. Its functional programming paradigm and built-in concurrency primitives made it well-suited for reliable and scalable telecom systems.</div>
</div>

<p>RabbitMQ comes with many customisations that are out of the box and introduce some features that can cater to various requirements. Some of the key features of RabbitMQ are-</p>
<ol>
<li><p><strong>Queues:</strong> This is a buffer where messages reside until a consumer consumes them. Queues are heavily configurable in RabbitMQ, like you can choose where to store the messages, in memory or on disk, should the messages persist a system restart etc. Multiple consumers can consume from the same queue.</p>
</li>
<li><p><strong>Exchanges:</strong> An exchange enforces rules on how to route messages to queues and in turn consumers based on routing rules. The exchange types are discussed later.</p>
</li>
<li><p><strong>Bindings:</strong> Binding is a connection between exchange and queues, the routing rules are defined here. The binding holds the <code>binding key</code>.</p>
</li>
<li><p><strong>Routing Keys:</strong> Routing keys are set by the producer to route messages to the desired queue(s).</p>
</li>
<li><p><strong>Acknowledgements:</strong> After processing the message, a consumer (worker) can send an acknowledgement (Ack) signal to RabbitMQ. In case of failed processing, the worker sends back a negative acknowledgement (Nack). RabbitMQ can be configured to re-queue or discard the message subject to requirement.</p>
</li>
</ol>
<p>Based on the routing rules, <code>exchanges</code> can be classified into four types -</p>
<ol>
<li><p><strong>Direct Exchange:</strong> Direct exchange compares the <code>routing key</code> of the message and the <code>binding key</code> of the message and only allows messages to pass to queues if there is an exact match between <code>routing key</code> and <code>binding key</code>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727886793650/46760c6f-7c90-4e3d-b28b-d70230bbc7b8.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Fanout Exchange:</strong> Fanout exchange pass messages to all bound queues, regardless of the <code>routing key</code> or <code>binding key</code>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727887557370/53d24032-026f-4dc9-82ef-0542b4ee4b5c.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Topic Exchange:</strong> Topic exchange is almost the same as <code>Direct Exchange</code>. But we match <code>routing key</code> and <code>binding key</code> using patterns here. If a <code>routing key</code> is <code>xyz.*</code> and <code>binding key</code> is <code>xyz.tango</code> or <code>xyz.foxtrot</code> then the topic exchange will match and forward the message.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727891873864/e64ac021-ae33-43de-b19a-a720261eac1a.png" alt class="image--center mx-auto" /></p>
<p> But if the binding key is <code>xyz.tango.zulu</code> it will not match <code>xyz.*</code>, because <code>*</code> matches a single word, while <code>#</code> matches multiple words. Let’s arrange a table to understand this better -</p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td>routing key</td><td>binding key</td></tr>
</thead>
<tbody>
<tr>
<td>xyz.abc</td><td>xyz.abc (matches) (mimics direct exchange)</td></tr>
<tr>
<td>xyz.*</td><td>xyz.tango (matches)</td></tr>
<tr>
<td>xyz.*</td><td>xyz.foxtrot (matches)</td></tr>
<tr>
<td>xyz.*</td><td>xyz.tango.zulu (does not match)</td></tr>
<tr>
<td>xyz.#</td><td>xyz.tango.zulu (matches)</td></tr>
<tr>
<td>#.zulu</td><td>xyz.tango.zulu (matches)</td></tr>
</tbody>
</table>
</div><ol start="4">
<li><p><strong>Header Exchange:</strong> Header exchange matches message headers and an exact match of header is needed to route the messages. Unlike a topic, there can be multiple headers of a message. To handle this, you can specify a special argument called <code>x-match</code> in the binding. Supported <code>x-match</code> values are -</p>
<ol>
<li><p><code>any</code>: If any one of the headers matches, route the message.</p>
</li>
<li><p><code>all</code>: If all of the headers match, then only route the message.</p>
</li>
</ol>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728032553611/fda10475-738d-4ac4-9107-8451a32634ad.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-celery-a-distributed-task-queue">Celery - a Distributed Task Queue🥬</h2>
<p>As we already had an introduction to task queues, we can start with different components of <code>Celery</code> and see how it works! But, as we have learned enough theory already, we can start with our project and do some hands-on! We can start with installing the dependencies.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728198470546/a6c46195-d09c-44ea-b88b-058ac53ab445.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-celery-dependencies">Celery dependencies</h3>
<p>As we already established, task queues need message brokers to communicate between producers/publishers and consumers/subscribers. Celery officially supports multiple brokers like -</p>
<ol>
<li><p>RabbitMQ</p>
</li>
<li><p>Redis</p>
</li>
<li><p>Amazon SQS</p>
</li>
</ol>
<p>We will be using RabbitMQ for our example. To install RabbitMQ, head over to <a target="_blank" href="https://www.rabbitmq.com/docs/download">rabbitmq.com/docs/download</a> and install it for your system of choice. I strongly suggest Ubuntu, Docker or any Linux distribution to use. I have tested my example project in Windows Subsystem for Linux(WSL) 2 - Ubuntu 24.04 distribution and MacOS Sequoia running in Macbook Air M1 (16GB).</p>
<p>Next, we have to install Celery, as it is a Python library we can easily install it using pip -</p>
<pre><code class="lang-bash">pip install celery
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Installing Celery directly in Windows is not recommended. Celery does not officially support Windows according to their official docs: <a target="_blank" href="https://docs.celeryq.dev/en/stable/faq.html#windows">https://docs.celeryq.dev/en/stable/faq.html#windows</a>. But Windows Subsystem for Linux or Docker on Windows can run celery without a problem.</div>
</div>

<h3 id="heading-the-most-basic-program-in-celery">The most basic program in Celery</h3>
<p>Now that we have installed our dependencies, we create a Python file called <code>celery_worker.py</code>.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery <span class="hljs-keyword">import</span> Celery

app = Celery(<span class="hljs-string">'hello'</span>, broker=<span class="hljs-string">'amqp://guest@localhost//'</span>)

<span class="hljs-meta">@app.task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hello</span>():</span>
    print(<span class="hljs-string">'Hello, World!'</span>)
</code></pre>
<p>Now that we have our starting file, I will explain it step by step -</p>
<ol>
<li><p>We are importing the main Celery class first.</p>
</li>
<li><p>Next, we instantiate Celery into a variable called <code>app</code>. The first argument is the name of the current module.</p>
</li>
<li><p>For the broker keyword argument, we pass the broker URL. For RabbitMQ, the template for the broker URL looks like this - <code>amqp://myuser:mypassword@localhost:5672/myvhost</code></p>
<p> Where <code>amqp://</code> is the main protocol supported by RabbitMQ, <code>myuser</code> is the username and <code>mypassword</code> is the username and password if credentials are set, <code>localhost:5672</code> is the domain/IP and port where the RabbitMQ server is running, and finally <code>myvhost</code> is the virtual host of RabbitMQ which helps segregate multiple applications using the same RabbitMQ server.</p>
<p> So, for our first celery program, we are connecting to a RabbitMQ broker running in <code>localhost</code> with the <code>guest</code> user, that does not require any password, and using the default vhost <code>/</code>.</p>
</li>
<li><p>Next, we create a new method called <code>hello</code> that prints <code>Hello, World!</code>. We are also annotating the method with <code>app.task</code> using the celery instance we created.</p>
</li>
</ol>
<p>Summarising what we did, we made a very basic celery config and declared a celery task. So we have two of the main components of a task queue configured. Now we will start with the consumer (or <code>worker</code>) first. To launch a celery <strong>worker</strong>, run this command from the same directory of <code>celery_worker.py</code> and observe the output -</p>
<pre><code class="lang-bash">$ celery -A celery_worker worker
 -------------- celery@forkbomb-pc v5.4.0 (opalescent)
--- ***** ----- 
-- ******* ---- Linux-5.15.146.1-microsoft-standard-WSL2-x86_64-with-glibc2.39 2024-10-04 22:52:46
- *** --- * --- 
- ** ---------- [config]
- ** ---------- .&gt; app:         hello:0x7fd1cd3b1bd0
- ** ---------- .&gt; transport:   amqp://guest:**@localhost:5672//
- ** ---------- .&gt; results:     disabled://
- *** --- * --- .&gt; concurrency: 12 (prefork)
-- ******* ---- .&gt; task events: OFF (<span class="hljs-built_in">enable</span> -E to monitor tasks <span class="hljs-keyword">in</span> this worker)
--- ***** ----- 
 -------------- [queues]
                .&gt; celery           exchange=celery(direct) key=celery
</code></pre>
<p>Though a Python module, celery is also available as an executable. In this command, <code>-A</code> denotes the module name (the filename without extension) and <code>worker</code> keyword asks celery to run a worker. Once the worker is started, it will wait for any task forever. The task is also created, but we now need a producer to enqueue the task.</p>
<p>Let us create another file called <code>producer.py</code>. We will import the Task from <code>celery_worker.py</code> here and enqueue the task.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery_worker <span class="hljs-keyword">import</span> hello
<span class="hljs-comment"># Enqueue the task</span>
hello.delay()
</code></pre>
<p><code>&lt;task_method&gt;.delay()</code> enqueue the task and after running <code>producer.py</code>, immediately you will see that the worker outputs the following -</p>
<pre><code class="lang-bash">[2024-10-04 23:02:42,342: WARNING/ForkPoolWorker-8] Hello, World!
</code></pre>
<p>If you can see the above output, then congratulations! You have successfully run your first Celery code.</p>
<p>But before we continue further doing advanced stuff, we need to discuss two key components of Celery: <strong>1. Backend (or Result Backend), 2. Worker concurrency models</strong>.</p>
<h3 id="heading-celery-result-backends">Celery Result Backends</h3>
<p>In our last example, we have seen the task only prints a line of text to stdout. But what if we want to enqueue a task that produces a result and returns it. Celery has provisions for defining a backend (or storage backend) that can store, retrieve, and monitor task results. The backend can also store task states like pending, success and failure along with task results. Celery backends can be broadly categorized like below -</p>
<ol>
<li><p><strong>Database Backends:</strong> Celery can store results in databases like PostgreSQL, MySQL, SQLite etc.</p>
</li>
<li><p><strong>Cache Backends:</strong> We can store results in in-memory caches like Redis or Memcached. These backends are usually the fastest.</p>
</li>
<li><p><strong>ORM Backends:</strong> ORMs or Object Relational Mappers abstract SQL queries to OOP-object or model operations. Celery supports Django ORM and SQLAlchemy as backends.</p>
</li>
<li><p><strong>Message Queue Backends:</strong> RabbitMQ, though designed as a broker, can act as a result backend too. Celery writes the task result to a temporary queue and the producer can read the task result from the queue.</p>
</li>
</ol>
<p>Considering the trade-offs, the fastest backends are cache (Redis or Memcached) but they offer limited persistence. But, if the task results are important, any DB or ORM backend will offer much better data persistence but will not be very performant under heavy load.</p>
<h3 id="heading-celery-worker-concurrency-models">Celery Worker Concurrency Models⚙️</h3>
<p>Concurrency in Celery Workers helps execute tasks in parallel. In my last <a target="_blank" href="https://blogs.snehangshu.dev/python-multitasking-key-practices-and-challenges">blog</a>, I discussed multiple ways to achieve parallel execution of tasks in Python. Celery uses similar concurrency models like -</p>
<ol>
<li><p><strong>Prefork:</strong> The main worker process of Celery launches multiple forked processes that can execute tasks. This worker model is the default, and as it is based on processes, this is recommended for CPU-bound tasks and common use cases. We do not need to specify the <code>prefork</code> worker using the <code>-P</code> flag while launching the worker as it is the default, but we can specify the number of forked processes one worker can have using the <code>—concurrency</code> flag, the default concurrency value for prefork worker is the number of CPU cores.</p>
<pre><code class="lang-bash"> celery -A celery_worker worker --concurrency=4
</code></pre>
</li>
<li><p><strong>Greenlets:</strong> Greenlets are lightweight threads that offer very high concurrency for running I/O-bound tasks. To run a celery worker with greenlets, we need either <a target="_blank" href="https://pypi.org/project/eventlet/"><code>eventlet</code></a> (older, not recommended) or <a target="_blank" href="https://pypi.org/project/gevent/"><code>gevent</code></a> (newer, maintained) library installed.</p>
<pre><code class="lang-bash"> celery -A celery_worker worker --concurrency=1000 -P eventlet
 celery -A celery_worker worker --concurrency=1000 -P gevent
</code></pre>
<p> Note as greenlets are very lightweight, we can easily run thousands of green threads for each worker.</p>
</li>
<li><p><strong>Threads:</strong> Celery uses Python threads to achieve concurrency and needs the Python <code>concurrent.futures</code> module to be present. Due to the GIL limitation of Python, this worker model is not recommended for CPU-bound workloads.</p>
<pre><code class="lang-bash"> celery -A celery_worker worker --concurrency=10 -P threads
</code></pre>
</li>
<li><p><strong>Solo:</strong> The solo worker executes one task at a time sequentially in the main worker process.</p>
<pre><code class="lang-bash"> celery -A celery_worker worker -P solo
</code></pre>
</li>
<li><p><strong>Custom:</strong> Adds support for custom worker pools for third-party implementations.</p>
</li>
</ol>
<h3 id="heading-getting-task-results-with-redis-backend">Getting Task Results with Redis Backend</h3>
<p>Now that we have established the required backend and worker knowledge, we can start implementing a backend. We will be using Redis - an in-memory database as a backend. To install Redis, follow the official guide from here: <a target="_blank" href="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/">https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728196535550/65bfb410-e8b1-4ed8-8bda-42f044e27f4b.png" alt class="image--center mx-auto" /></p>
<p>We also need to install the <a target="_blank" href="https://pypi.org/project/redis/"><code>redis</code></a> client library so that Python can connect to Redis -</p>
<pre><code class="lang-bash">pip install redis
</code></pre>
<p>Next, we can change our <code>celery_worker.py</code> to include the Redis backend running in <code>localhost:6379</code> and db <code>0</code> -</p>
<pre><code class="lang-python">app = Celery(<span class="hljs-string">'hello'</span>, backend=<span class="hljs-string">'redis://localhost:6379/0'</span>, broker=<span class="hljs-string">'amqp://guest@localhost//'</span>)
</code></pre>
<p>Let’s modify the task method to take a number as input and return its square -</p>
<pre><code class="lang-python"><span class="hljs-meta">@app.task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">square</span>(<span class="hljs-params">x</span>):</span>
    <span class="hljs-keyword">return</span> x * x
</code></pre>
<p>In the producer, we can now enqueue the task -</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery_worker <span class="hljs-keyword">import</span> square

result = square.delay(<span class="hljs-number">12</span>)

print(result.get())
<span class="hljs-comment"># Outputs 144</span>
</code></pre>
<p>We have now executed two programs and have seen how to schedule tasks and get task results using a backend. The <code>&lt;task_method&gt;.delay()</code> call replicates the method signature and we can pass both method argument and keyword arguments to it.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can try removing the backend argument to Celery and try to get the results. The task will fail complaining that a backend is not available!</div>
</div>

<p>There is another more advanced method called <code>apply_async()</code> that supports task chaining, callbacks, errbacks (code to execute if the task fails), execution time and expiry. Visit the official Celery doc to learn more about <a target="_blank" href="https://docs.celeryq.dev/en/main/userguide/calling.html"><code>apply_async()</code></a>.</p>
<h2 id="heading-video-transcoding-with-fastapi-celery-and-ffmpeg">Video🎥 Transcoding with FastAPI, Celery and FFmpeg</h2>
<p>From the YouTube example in the Introduction, the video transcoding happens in the background while you add a title, some description, tags etc. and can take many hours depending on your original video quality.</p>
<p>Video transcoding (or downscaling here) is a compute-intensive task that is heavily CPU-bound (or GPU-bound if you use GPU as an accelerator). Distributed task queues can be a perfect fit to solve this problem statement.</p>
<h3 id="heading-ingredients-for-our-project">Ingredients for our project</h3>
<p>We will be using the following tools for our project -</p>
<ol>
<li><p>Celery</p>
</li>
<li><p>RabbitMQ as broker</p>
</li>
<li><p>Redis as a result backend</p>
</li>
<li><p>FastAPI for our web framework</p>
</li>
<li><p>FFmpeg - an open-source tool to record, stream, and edit audio, video and other multimedia files. Install it in your system of choice from the official <a target="_blank" href="https://www.ffmpeg.org/download.html">page</a>.</p>
</li>
</ol>
<p>Our <code>requirements.txt</code> looks like this -</p>
<pre><code class="lang-python">celery~=<span class="hljs-number">5.4</span><span class="hljs-number">.0</span> <span class="hljs-comment"># Celery library</span>
fastapi[standard]~=<span class="hljs-number">0.115</span><span class="hljs-number">.0</span> <span class="hljs-comment"># FastAPI and its standard dependencies</span>
ffmpeg-python~=<span class="hljs-number">0.2</span><span class="hljs-number">.0</span> <span class="hljs-comment"># Python library for ffmpeg</span>
redis~=<span class="hljs-number">5.1</span><span class="hljs-number">.0</span> <span class="hljs-comment"># Python library for connecting to redis</span>
</code></pre>
<h3 id="heading-worker-setup">Worker setup</h3>
<p>Please clone my <a target="_blank" href="https://github.com/f0rkb0mbZ/celery-video-transcoder">repository</a> before proceeding. As this blog is getting longer, it will not be possible to explain every line of source code. I will explain the key syntaxes and the source code will be well commented.</p>
<p><code>celery_worker.py</code></p>
<pre><code class="lang-bash">from celery import Celery

app = Celery(<span class="hljs-string">'celery-video-transcoder'</span>,
             backend=<span class="hljs-string">'redis://localhost:6379/0'</span>, 
             broker=<span class="hljs-string">'amqp://guest@localhost//'</span>,
             include=[<span class="hljs-string">'helpers.video'</span>])
</code></pre>
<p>We are connecting to RabbitMQ as a broker and Redis as a backend with their default settings. However, our tasks are not defined in the same file in the project. To make Celery find the tasks, we need to pass an <code>include</code> argument with the module where the tasks are defined. Our task <code>resize_video_with_progress</code> is defined in <code>helpers/video.py</code>.</p>
<h3 id="heading-api-routes">API Routes</h3>
<p>In the <code>api.py</code>, we have three main routes -</p>
<ol>
<li><p><code>/</code>: We host the home page here which takes a video file as input.</p>
</li>
<li><p><code>/upload_file/</code>: Used for handling upload, generating the video thumbnail, and enqueueing a video downscaling task for each quality setting from <code>2160p</code> to <code>144p</code>.</p>
</li>
<li><p><code>/task_status/{task_id}/</code>: Used for polling video conversion progress to show the progress in the front end.</p>
</li>
</ol>
<h3 id="heading-about-ffmpeg">About FFmpeg</h3>
<p>FFmpeg doesn't come with a GUI or library by default. To use it with Python, we are using the <a target="_blank" href="https://pypi.org/project/ffmpeg-python/"><code>ffmpeg-python</code></a> library. FFmpeg creates filter graphs for any audio or video operation. Complex filter graphs, with many operations like trimming, merging, mixing, cropping, and adding overlays, can be difficult to manage in the command line. However, the Python library offers a simple syntax to handle these operations.</p>
<p>For example, to downscale a video with a <code>scale</code> filter named <code>rickroll.mp4</code> to <code>480p</code> has a syntax like this -</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> ffmpeg

ffmpeg.input(<span class="hljs-string">'rickroll.mp4'</span>).filter(<span class="hljs-string">'scale'</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">480</span>).output(<span class="hljs-string">'rickroll_480p.mp4'</span>).run()
</code></pre>
<p>Here the scale filter takes two arguments, the first is <code>width</code> and the second is <code>height</code>. To scale any video to <code>480p</code> we set the height to <code>480</code> and set the with to <code>-1</code>✳️ to let ffmpeg auto-calculate it.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">✳</div>
<div data-node-type="callout-text">There is a quirk in setting the width to <code>-1</code>. If the final calculated width is not divisible by 2, the scale filter will fail. Usually, we calculate the output width by multiplying the output height with the original aspect ratio (width/height). So to get a value divisible by 2, we can divide the output width by 2, round off the value and again multiply by 2. The formula is expressed as <code>trunc(oh * (iw / ih) / 2) * 2</code> where <code>oh = output height</code>, <code>iw = input width</code> and <code>ih = input height</code>.</div>
</div>

<p>Also using the same scale filter and only taking out a single frame from a random timestamp of the video we can extract a thumbnail.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> random

file_path = <span class="hljs-string">'rickroll.mp4'</span>
thumbnail_path = <span class="hljs-string">'rickroll_thumbnail.jpg'</span>

ffmpeg.input(file_path, ss=random.randint(<span class="hljs-number">0</span>, int(float(media_length)))).filter(<span class="hljs-string">'scale'</span>, <span class="hljs-number">1280</span>, <span class="hljs-number">-1</span>).output(
                thumbnail_path, vframes=<span class="hljs-number">1</span>).overwrite_output().run()
</code></pre>
<p>I have also used <code>ffmpeg.probe(&lt;file_path&gt;)</code> to extract codec information from the input file which contains video dimension and length.</p>
<p>I added two types of filters, one for normal CPU <code>encoding</code> (with the scale filter) and another one that uses <code>CUDA</code> and <code>nvenc</code> encoder to accelerate the process using NVIDIA GPUs. There is a <code>cuda</code> flag that enables GPU encoding in our task method. Video encoding with <code>nvenc</code> is out of the scope of this blog.</p>
<h3 id="heading-kowalski-status-report">Kowalski, status report!🐧</h3>
<p>Celery tracks the status of a task with six predefined states: <code>PENDING</code>, <code>STARTED</code>, <code>SUCCESS</code>, <code>FAILURE</code>, <code>RETRY</code>, <code>REVOKED</code>. To implement status polling from the UI and track the video transcoding progress, I have implemented a custom task state called <code>PROGRESS</code>.</p>
<p>We can call the <code>update_state</code> method from inside the task, that periodically sets a state and marks the progress. I am setting the state <code>PROGRESS</code> and setting a dictionary with a key <code>progress</code> with the percentage of the task completed.</p>
<pre><code class="lang-python">self.update_state(state=<span class="hljs-string">'PROGRESS'</span>, meta={<span class="hljs-string">'progress'</span>: percent_complete})
</code></pre>
<p>To read the task status, I am sending the enqueued task IDs to the front end and polling the <code>/task_status/{task_id}</code> endpoint to get the task status.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> celery.result <span class="hljs-keyword">import</span> AsyncResult

task = AsyncResult(task_id)
<span class="hljs-keyword">if</span> task.state == <span class="hljs-string">'PROGRESS'</span>:
    task.info.get(<span class="hljs-string">'progress'</span>, <span class="hljs-number">0</span>)
</code></pre>
<h3 id="heading-the-demo">The demo!</h3>
<p>To run the demo on your machine you can clone the repository here: <a target="_blank" href="https://github.com/f0rkb0mbZ/celery-video-transcoder">https://github.com/f0rkb0mbZ/celery-video-transcoder</a>. I have provided a <code>docker-compose.yml</code> file to make the setup of Redis and Rabbitmq easier. Please pardon my poor UI and javascript skills as I am not a frontend developer. In the demo, I have used the internet’s favourite video in 1080p to downscale it to 720p, 480p, 360p, 240p and 144p, also it generates a thumbnail as a bonus!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/zcE0rbKOsWU">https://youtu.be/zcE0rbKOsWU</a></div>
<p> </p>
<h2 id="heading-ending-note">Ending note✍🏼</h2>
<p>In conclusion, distributed computing with Python, particularly using Celery, offers a robust solution for building scalable applications capable of handling millions of tasks efficiently. We have learned that -</p>
<ol>
<li><p>By leveraging task queues and message brokers like RabbitMQ, developers can achieve significant horizontal scalability and decoupling of application components.</p>
</li>
<li><p>Celery's integration with various backends and concurrency models provides flexibility in managing task results and optimizing performance for different workloads.</p>
</li>
<li><p>The practical example of video transcoding demonstrates Celery's capability to handle compute-intensive tasks, showcasing its potential in real-world applications.</p>
</li>
</ol>
<p>As you explore Celery further, you'll find it a valuable tool in your distributed computing toolkit, enabling you to build resilient and scalable systems.</p>
<p>It took a lot of time and effort for me to gather all this information and write this blog. If you've made it this far and appreciate my effort, please give it a 💚. Share this blog with anyone who might find it useful. If you have any questions or need help understanding any concept, feel free to ask in the comments section! Thank you for reading!</p>
<h2 id="heading-references">References📋</h2>
<ol>
<li><p><a target="_blank" href="https://docs.celeryq.dev/en/stable/"><strong>The Official Celery Documentation</strong></a></p>
</li>
<li><p><a target="_blank" href="https://fastapi.tiangolo.com/"><strong>The Official FastAPI Documentation</strong></a></p>
</li>
<li><p>Blogs of <a target="_blank" href="https://hashnode.com/@bstiel"><strong>Bjoern Stiel</strong></a> in <a target="_blank" href="https://celery.school/"><strong>celery.school</strong></a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Python Multitasking: Key Practices and Challenges]]></title><description><![CDATA[When I first encountered the Python language, I was amazed by what you could do with so little code, and it has been one of my favourite programming languages since. Some time ago, I faced a problem where I was trying to create something like a web s...]]></description><link>https://blogs.snehangshu.dev/python-multitasking-key-practices-and-challenges</link><guid isPermaLink="true">https://blogs.snehangshu.dev/python-multitasking-key-practices-and-challenges</guid><category><![CDATA[greenlets]]></category><category><![CDATA[Python]]></category><category><![CDATA[multitasking]]></category><category><![CDATA[multithreading]]></category><category><![CDATA[GIL]]></category><category><![CDATA[Global interpreter lock]]></category><category><![CDATA[multiprocessing]]></category><category><![CDATA[Threads]]></category><category><![CDATA[Threading]]></category><category><![CDATA[process]]></category><category><![CDATA[celery]]></category><category><![CDATA[asyncio]]></category><category><![CDATA[asynchronous programming]]></category><category><![CDATA[gevent]]></category><category><![CDATA[Event Loop]]></category><dc:creator><![CDATA[Snehangshu Bhattacharya]]></dc:creator><pubDate>Sat, 21 Sep 2024 10:37:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726914733584/42f3933a-ea96-4fc5-af46-c9b3b2bbff7b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I first encountered the Python language, I was amazed by what you could do with so little code, and it has been one of my favourite programming languages since. Some time ago, I faced a problem where I was trying to create something like a web server, but with UDP streams.</p>
<p>It was a network programming challenge, similar to building a high-performance, low-latency server. To achieve this with Python, I had to experiment with various multitasking technologies such as concurrency, parallelism, threads, processes, asynchronous programming, and greenlets. This is the first part of a two-part blog and this one covers the most common techniques that I learned for achieving multitasking in Python. I will discuss Celery, a distributed task queue in detail in the next part of this blog.</p>
<h2 id="heading-basic-multitasking-foundations">Basic Multitasking Foundations</h2>
<p>We often use words like 'multitasking,' 'concurrency,' 'parallelism,' 'thread,' and 'process' interchangeably. While they all aim to achieve similar goals, their internal workings can be quite different. Let's review them one by one -</p>
<ol>
<li><p><strong>Multitasking:</strong> Performing multiple tasks at the same time, or the execution by a computer of more than one program or task simultaneously.</p>
<p> <img src="https://images.odishatv.in/uploadimage/library/16_9/16_9_2/IMAGE_1660131834.webp" alt="Multitasking by Viru Sahastrabudhhe from the movie 3 Idiots" class="image--center mx-auto" /></p>
</li>
<li><p><strong>CPU Bound and I/O Bound Tasks:</strong> Tasks that involve computation by CPU are called CPU-bound tasks. Examples include arithmetic operations, image/video processing etc.</p>
<p> Tasks that involve input/output operations, where waiting for data is needed, are called I/O-bound tasks. Examples include reading/writing to disk, network requests, database queries etc.</p>
</li>
<li><p><strong>Concurrency:</strong> It can be defined as a computer's ability to execute different tasks at the same time✳️ by switching between multiple tasks without necessarily completing them. This behaviour is called context switching and we call the tasks as threads. Threads can be implemented in Python using the <code>threading</code> library and <code>ThreadPoolExecutor</code> from <code>concurrent.futures</code>. Threads are created within the same process, sharing code, data, and resources. They are lightweight.</p>
<pre><code class="lang-python"> <span class="hljs-keyword">import</span> threading

 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>():</span>
     <span class="hljs-comment"># Your task here</span>

 thread = threading.Thread(target=task)
 thread.start()
</code></pre>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">✳</div>
 <div data-node-type="callout-text">Modern CPUs can run at frequencies 2-5 GHz. They can switch between threads so fast, that a single CPU core can handle a lot of simultaneous tasks that feel like multitasking.</div>
 </div>

<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724699595118/b1ebca27-c5b6-4676-8fb5-6a695cac609f.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Parallelism:</strong> Parallel execution of tasks is only possible when there are more than one CPU core is present. One CPU core can handle a task while the other core handles another. In Python, we can implement parallelism with the help of <code>multiprocessing</code> library and <code>ProcessPoolExecutor</code> from <code>concurrent.futures</code>.</p>
<pre><code class="lang-python"> <span class="hljs-keyword">import</span> multiprocessing

 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>():</span>
     <span class="hljs-comment"># Your task here</span>

 process = multiprocessing.Process(target=task)
 process.start()
</code></pre>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">💡</div>
 <div data-node-type="callout-text">The first desktop-class dual-core processor was the Intel Pentium D which was quickly followed by AMD with their Athlon 64 X2 in the year 2005!</div>
 </div>

<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724700574073/9d45a83b-0b1d-454c-a59d-0075e3997e0e.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Concurrent and Parallel:</strong> When a CPU schedules multiple tasks over multiple cores, it benefits from both concurrency and parallelism. While some programming languages like Java and C++ can multiplex threads to run on different CPU cores and achieve concurrent parallel multitasking, this is not possible in Python due to Global Interpreter Lock (GIL). More on this later.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725121215870/87a4432b-bb2b-4ffb-92a8-e0cc934b5162.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>AsyncIO:</strong> Introduced in Python 3.4, asyncio is a built-in library for asynchronous programming that runs an event loop that is capable of running multiple asynchronous tasks and coroutines at the same time. You write your functions as coroutines that start with <code>async def</code> and can be paused and resumed. When your function encounters any I/O operation it yields back control to the event loop and the event loop continues executing other tasks. When the I/O operation completes, it triggers a callback and the event loop picks up the task again.</p>
<p> The I/O operation is executed by the operating system. The event loop uses low-level I/O polling mechanisms like <code>epoll</code>, <code>select</code>, <code>kqueue</code> etc. based on the OS, to manage task completion notifications.</p>
<pre><code class="lang-python"> <span class="hljs-keyword">import</span> asyncio

 <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>():</span>
     <span class="hljs-comment"># Your async task here</span>

 asyncio.run(task())
</code></pre>
<p> Though <code>asyncio</code> is extremely good for I/O bound tasks (like network programming), as the main event loop is single-threaded, this mechanism is not suited for CPU-bound or compute-heavy tasks.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726908963470/f8bb775f-8d6c-4499-b3f4-f53f9e51ebc6.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-problem-with-multithreading-in-python">Problem with Multithreading in Python</h2>
<p>The biggest challenge with multitasking in Python is <strong>Global Interpreter Lock (GIL).</strong> As we saw earlier, threads can run in a truly parallel manner in multi-core systems. In languages like C++ and Java, threads with CPU-bound tasks can run on different CPU cores simultaneously.</p>
<p>GIL is a mutex that prevents native threads from executing Python bytecode simultaneously in CPython (the most common Python language implementation). GIL ensures that even in multi-core systems, only one thread runs in the interpreter.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726904089576/4585630c-9407-4325-ad71-61461d57240c.jpeg" alt class="image--center mx-auto" /></p>
<p>Due to the GIL, threads in Python are not effective for CPU-bound tasks and can sometimes degrade performance because of context-switching overhead. The easiest way to bypass the GIL is to use <code>multiprocessing</code>, but this comes with overheads in startup time and memory. For I/O-bound tasks (like network requests and disk I/O), threads can still be effective because the GIL is released during I/O operations.</p>
<h2 id="heading-other-solutions">Other Solutions</h2>
<p>There are more ways that you can implement multitasking in Python. Some of them are <strong>Greenlets</strong> (with third-party libraries like <code>eventlet</code> and <code>gevent</code>), <strong>Subinterpreters</strong> (a new way of multitasking with Python 3.12+), <strong>Celery</strong> (scalable, distributed task queue) etc.</p>
<h3 id="heading-before-asyncio">Before Asyncio</h3>
<p>We already talked about how asyncio with its event loop model improves the execution of I/O-bound tasks in Python. Some leading Python web frameworks like FastAPI and AioHTTP are made using asyncio and can handle thousands of connections in a single thread.</p>
<p>But, before Python 3.4 there was no asyncio (and no <code>async def</code> and <code>await</code> syntax). To build web servers, popular models like pre-fork and threading were used -</p>
<ol>
<li><p><strong>Pre-fork:</strong> The web server starts with a main process, and forks multiple child processes. Each child process then waits for an incoming connection. Apache web server with <code>mpm_prefork</code> module is an example of this.</p>
</li>
<li><p><strong>Threading:</strong> The main server process starts and spawns multiple worker threads. These worker threads handle the incoming connections. Once the connection is responded to, the worker thread becomes available again. Apache web server with <code>mpm_worker</code> module is an example of this.</p>
</li>
</ol>
<h3 id="heading-greenlets-eventlet-and-gevent">Greenlets, Eventlet and Gevent</h3>
<p><a target="_blank" href="https://pypi.org/project/eventlet/">Eventlet</a> is a Python library that allows you to write concurrent code using a blocking style of programming (no async/await). It was created when there was no implementation of asyncio in Python - about 18 years ago.</p>
<p>Eventlet is not as updated and maintained nowadays as Python’s asyncio grew and its use is discouraged. <a target="_blank" href="https://pypi.org/project/gevent/">Gevent</a> is a newer library that implements green threads for massively concurrent workloads like web servers. Gevent can run thousands of green threads under a single OS-based thread.</p>
<p>The working mechanism of Eventlet or Gevent is something like this:</p>
<ol>
<li><p><strong>Green Threads:</strong> It uses something called <code>green threads</code> or <code>greenlets</code> which are lightweight threads managed by the library itself (not by the operating system).</p>
</li>
<li><p><strong>Cooperative multitasking:</strong> Green threads voluntarily yield control to other green threads which is called as cooperative multitasking.</p>
</li>
<li><p><strong>Event loop:</strong> Just like asyncio, Eventlet or Gevent has an event loop that manages the execution of the green threads.</p>
</li>
<li><p><strong>Monkey(🐒) Patching:</strong> Eventlet/Gevent can make changes in the Python standard library to make blocking operations non-blocking. This is called monkey patching. For example, network operations and file I/O will be able to yield control to other green threads while waiting for network response or file read operations. There are some strict rules for monkey patching to work, like -</p>
<ol>
<li><p>Monkey patching should be done at the beginning of the code but after the imports so that Eventlet / Gevent can patch the blocking methods of those imports. Any imports after patching can cause issues because they might contain unpatched methods.</p>
<pre><code class="lang-python"> <span class="hljs-comment"># Any other imports</span>
 <span class="hljs-keyword">import</span> eventlet
 eventlet.monkey_patch()

 <span class="hljs-comment"># Any other imports</span>
 <span class="hljs-keyword">from</span> gevent <span class="hljs-keyword">import</span> monkey
 monkey.patch_all()
</code></pre>
</li>
<li><p>Always perform monkey patching on the main thread so that all greenlets can use the patched functions.</p>
</li>
<li><p>Monkey Patching might not work for any third-party library other than the standard library. Check if the third-party library is supported for patching by Eventlet/Gevent first.</p>
</li>
</ol>
</li>
</ol>
<p>There are small examples given below for both Eventlet and Gevent. You might notice no monkey patching done in the examples because the code does not use any blocking function from the standard library.</p>
<pre><code class="lang-python"><span class="hljs-comment"># eventlet example</span>
<span class="hljs-keyword">import</span> eventlet

<span class="hljs-comment"># Define a task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>(<span class="hljs-params">name</span>):</span>
    print(<span class="hljs-string">f"<span class="hljs-subst">{name}</span> started"</span>)
    eventlet.sleep(<span class="hljs-number">1</span>)
    print(<span class="hljs-string">f"<span class="hljs-subst">{name}</span> finished"</span>)

pool = eventlet.GreenPool()
pool.spawn(task, <span class="hljs-string">"Task 1"</span>)
pool.spawn(task, <span class="hljs-string">"Task 2"</span>)
pool.waitall()

<span class="hljs-comment"># gevent example</span>
<span class="hljs-keyword">import</span> gevent

<span class="hljs-comment"># Define a task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>(<span class="hljs-params">name</span>):</span>
    print(<span class="hljs-string">f"<span class="hljs-subst">{name}</span> started"</span>)
    gevent.sleep(<span class="hljs-number">1</span>)
    print(<span class="hljs-string">f"<span class="hljs-subst">{name}</span> finished"</span>)

gevent.joinall([
    gevent.spawn(task, <span class="hljs-string">"Task 1"</span>),
    gevent.spawn(task, <span class="hljs-string">"Task 2"</span>)
])
</code></pre>
<h3 id="heading-subinterpreters">Subinterpreters</h3>
<p>By now, we know that the Python interpreter is single-threaded. However, there is another way to run code in parallel within the main interpreter, which is by using isolated execution units. This is called sub-interpreters and has been a part of Python for a long time. Global Interpreter Lock was never implemented at the Interpreter level until recently when Python 3.12 was released. From Python 3.12 each (sub)-interpreter has its own GIL and can run under the main Python process parallel.</p>
<p>From this point, I should warn the readers that the high-level subinterpreters API available in Python 3.12 is experimental and should not be used in production workloads.</p>
<p>To run a subinterpreter with Python 3.12, we can import a hidden module -</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> _xxsubinterpreters <span class="hljs-keyword">as</span> interpreters

<span class="hljs-comment"># Create a new sub interpreter and store its id</span>
interp1 = interpreters.create()

<span class="hljs-comment"># Run python code in the interpreter</span>
interpreters.run_string(interp1, <span class="hljs-string">'''print('Hello, World!')'''</span>)
</code></pre>
<p>In the later versions of Python, we will be able to access this API with <code>import interpreters</code>. Compared to multiprocessing, subinterpreters have less overhead but are heavier than threads/coroutines/greenlets. Though experimental, subinterpreters can make way for true parallelism in Python in future releases.</p>
<h3 id="heading-celery">Celery</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726906965419/40db82d7-ffda-4c64-b67c-786fb430895d.gif" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://docs.celeryq.dev/">Celery</a> is an asynchronous distributed task queue that can truly run Python code in scale (or in parallel, or concurrent parallel). By nature, task queues, process tasks in a distributed way (from threads to different machines). Like other task queues, Celery communicates with messages between clients (who schedule a task) and workers (who execute a task) with the help of a message broker like Redis, RabbitMQ, etc. With multiple brokers and workers, Celery can support High Availability and Horizontal Scaling.</p>
<p>The next part of this blog will discuss Celery in detail.</p>
<h2 id="heading-wrapping-up">Wrapping up!</h2>
<p>In this article, we explored various multitasking techniques in Python, including concurrency, parallelism, threading, multiprocessing, and asynchronous programming with asyncio. We also discussed the challenges posed by the Global Interpreter Lock (GIL) and alternative solutions like greenlets, subinterpreters, and libraries such as Eventlet and Gevent. Understanding these concepts is crucial for building efficient applications, especially for network programming and I/O-bound tasks. To keep the blog concise, I focused on the brief theoretical aspects, as each multitasking paradigm could fill an entire book. You can follow the given references to learn more about each of them.</p>
<p>In the next part, we will dive deeper into Celery, a powerful distributed task queue, to achieve scalable and parallel task execution in Python. Stay tuned!</p>
<p>Also, please drop a 💙 if you made it this far and appreciate my effort and share this blog with anyone who might find it helpful. Thank you for reading!</p>
<h2 id="heading-references">References</h2>
<ol>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=RlM9AfWf1WU">Concurrency Vs Parallelism! by ByteByteGo</a></p>
</li>
<li><p><a target="_blank" href="https://realpython.com/async-io-python/">Async IO in Python: A Complete Walkthrough by RealPython</a></p>
</li>
<li><p><a target="_blank" href="https://sdiehl.github.io/gevent-tutorial/">gevent For the Working Python Developer by the Gevent Community</a></p>
</li>
<li><p><a target="_blank" href="https://upsun.com/blog/python-gevent-best-practices/">Python Gevent in practice: common pitfalls to keep in mind</a></p>
</li>
<li><p><a target="_blank" href="https://tonybaloney.github.io/posts/sub-interpreter-web-workers.html">Running Python Parallel Applications with Sub Interpreters by Anthony Shaw</a></p>
</li>
<li><p><a target="_blank" href="https://thinhdanggroup.github.io/subinterpreter/">Python 3.12 Subinterpreters: A New Era of Concurrency by Thinh Dang</a></p>
</li>
<li><p><a target="_blank" href="https://excalidraw.com/">excalidraw.com</a> is where I made the diagrams for this blog</p>
</li>
<li><p>Easter Egg: Michaelsoft Binbows (<a target="_blank" href="https://knowyourmeme.com/memes/michaelsoft-binbows">knowyourmeme</a>, <a target="_blank" href="https://www.youtube.com/watch?v=QRIklga9IBQ">youtube</a>)</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[How to Easily Set Up an HTTPS Reverse Proxy with Caddy for Beginners]]></title><description><![CDATA[Have you ever tried to set up a reverse proxy with Nginx and encountered issues? It happened to me frequently until I learned the intricacies. In this blog, I will demonstrate the easiest way to set up a reverse proxy with HTTPS using Caddy, an Nginx...]]></description><link>https://blogs.snehangshu.dev/effortless-https-reverse-proxy-setup-with-caddy</link><guid isPermaLink="true">https://blogs.snehangshu.dev/effortless-https-reverse-proxy-setup-with-caddy</guid><category><![CDATA[Caddy]]></category><category><![CDATA[Reverse Proxy]]></category><category><![CDATA[https]]></category><category><![CDATA[nginx]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[web server]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Let's Encrypt]]></category><category><![CDATA[certbot]]></category><category><![CDATA[SSL]]></category><category><![CDATA[SSL Certificate]]></category><dc:creator><![CDATA[Snehangshu Bhattacharya]]></dc:creator><pubDate>Sun, 28 Jul 2024 09:48:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722155711704/948cfd9a-eac4-4099-9aae-f7786063e4ae.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever tried to set up a reverse proxy with <strong>Nginx</strong> and encountered issues? It happened to me frequently until I learned the intricacies. In this blog, I will demonstrate the easiest way to set up a reverse proxy with HTTPS using <strong>Caddy</strong>, an <strong>Nginx</strong> alternative. But first, what is a reverse proxy?</p>
<h3 id="heading-reverse-proxy">Reverse Proxy</h3>
<p>A reverse proxy is a server that sits between a client and one or more web servers while intercepting connections from client to server. It can manipulate request data, load balance requests, protect from cyber attacks and many more!</p>
<p><img src="https://cf-assets.www.cloudflare.com/slt3lc6tev37/3msJRtqxDysQslvrKvEf8x/f7f54c9a2cad3e4586f58e8e0e305389/reverse_proxy_flow.png" alt="Reverse proxy flow: traffic flows from user's device (D) to Internet to reverse proxy (E) to origin server (F)" /></p>
<h3 id="heading-nginx-example">Nginx Example</h3>
<p>Let's take an example of a Nginx config file. The example shows a reverse proxy that listens on the default HTTP port 80, forwards requests to an upstream <code>http://localhost:8080</code> and modifies some request headers along the way.</p>
<pre><code class="lang-nginx"><span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;

    <span class="hljs-attribute">server_name</span> example.com;

    <span class="hljs-attribute">location</span> / {
        <span class="hljs-attribute">proxy_pass</span> http://localhost:8080;
        <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
    }
}
</code></pre>
<p>If you already know Nginx, this is the most basic reverse proxy configuration you can use. We define a few things here:</p>
<ol>
<li><p><strong>server Block:</strong> This defines a virtual server. Multiple server blocks can route connections based on IP, port and server_name.</p>
</li>
<li><p><strong>listen:</strong> Port to listen to.</p>
</li>
<li><p><strong>server_name:</strong> Nginx matches the <code>Host</code> header of the connection to this and routes the request.</p>
</li>
<li><p><strong>location block:</strong> A location block is used to reach different resources in a server block.</p>
</li>
<li><p><strong>proxy_pass:</strong> This delegate requests or connections to one or more upstream servers.</p>
</li>
<li><p><strong>proxy_set_header:</strong> This directive is used to edit/add/remove headers to the upstream requests.</p>
</li>
</ol>
<h3 id="heading-generating-ssl-certificate">Generating SSL certificate</h3>
<p>There are many <strong>Certificate Authority (CA)</strong> that issue <strong>SSL</strong> certificates. CAs are trusted issuers of digital certificates. If you ask how we determine which CAs are trusted, for most of us browsing the internet, It is the browser or OS vendor like Google, Microsoft, Mozilla, Apple etc. However, there is a governing forum called <a target="_blank" href="https://cabforum.org/">CA/Browser Forum</a> that decides additional rules and regulations for CAs.</p>
<p>For our example, we can use a tool called <a target="_blank" href="https://certbot.eff.org/"><strong>Certbot</strong></a><strong>,</strong> a tool from the Electronic Frontier Foundation that generates SSL certificates from a non-profit certificate authority called <a target="_blank" href="https://letsencrypt.org/">Let's Encrypt</a>.</p>
<p>You can go to <a target="_blank" href="https://certbot.eff.org/">certbot.eff.org</a> and install certbot for your system by following their guide. Once certbot is installed, just run -</p>
<pre><code class="lang-bash">$ sudo certbot —-nginx
</code></pre>
<p>This will automatically verify your domain (you have to set your domain name to <code>server_name</code>), request a certificate from Let's Encrypt CA and modify your Nginx configuration to enable HTTPS for your server block.</p>
<h3 id="heading-caddy-server">Caddy Server</h3>
<p>Just like Nginx, <strong>Caddy</strong> is a cross-platform, open-source web server. While Nginx is very fast and a big name in the industry, <strong>Caddy</strong> has some awesome features like -</p>
<ol>
<li><p><strong>Automatic HTTPS:</strong> Caddy, by default obtains/renews SSL/TLS certificates from Let's Encrypt / ZeroSSL. Even for <code>localhost</code> or, internal addresses, Caddy can provision short-lived, auto-renewing certificates using a locally trusted certificate authority.</p>
</li>
<li><p><strong>Simple Configuration Syntax:</strong> Caddy's configuration file - <code>Caddyfile</code> is an extremely simple and human-readable format compared to the Nginx configuration syntax.</p>
</li>
<li><p><strong>Built-in static file server:</strong> Serve compressed files or compress on the go to save bandwidth. Serve static sites from a database, local file system or remote cloud storage. Also, serving a directory (that does not have an index file) looks like this:</p>
<p> <img src="https://caddyserver.com/resources/images/file-browser/browse-themes.png" alt="Caddy File Server" /></p>
<p> To start a local file server, run <code>caddy file-server</code> in a directory that you want to serve.</p>
</li>
<li><p><strong>REST API for configuration:</strong> Caddy has a REST API that can be used to make dynamic configuration changes.</p>
</li>
<li><p><strong>Extensibility:</strong> Caddy has a huge <a target="_blank" href="https://caddyserver.com/docs/modules/">library</a> of pre built modules that can be used to supercharge it's capabilities.</p>
</li>
<li><p>and many more...</p>
</li>
</ol>
<h3 id="heading-caddy-example">Caddy Example</h3>
<p>This blog is not a getting-started or how-to guide for Caddy, so I will stick to the very basic Nginx example and try to recreate that in Caddy, step by step.</p>
<ol>
<li><p><strong>Install Caddy:</strong> Install caddy for your platform by following the official <a target="_blank" href="https://caddyserver.com/docs/install">guide</a>.</p>
</li>
<li><p><strong>Write Caddyfile:</strong> The Caddy configuration file is written in a file called <code>Caddyfile</code>, no extension. If we translate the same Nginx config above to Caddy in the simplest form, it will look like this -</p>
<pre><code class="lang-plaintext"> :80

 reverse_proxy http://localhost:8080
</code></pre>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">💡</div>
 <div data-node-type="callout-text">Caddy also has support for JSON configuration. For complex setups JSON is preferred.</div>
 </div>
</li>
<li><p><strong>Run Caddy:</strong> From the same directory of the <code>Caddyfile</code> use <code>caddy run</code> to start caddy.</p>
</li>
<li><p><strong>Browse the page:</strong> Open <code>http://127.0.0.1</code> in the browser. Caddy will now be proxying the traffic to <code>http://localhost:8080</code>.</p>
</li>
</ol>
<h3 id="heading-automatic-https">Automatic HTTPS</h3>
<p>To enable automatic HTTPS, we need to tweak the <code>Caddyfile</code> a little, by including a domain name -</p>
<pre><code class="lang-plaintext">example.com

reverse_proxy http://localhost:8080
</code></pre>
<p>If you run the above <code>Caddyfile</code>, caddy will try to generate a certificate for the domain <code>example.com</code>, and it will fail, as you do not own <code>example.com</code>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Example domains like example.com are only used for documentation purposes, and are not available to purchase or transfer.</div>
</div>

<p>If you change the domain name from <code>example.com</code> to <code>localhost</code> and then run Caddy, caddy will generate a new self-signed certificate for localhost and add it to the local trust store. So in the browser, it looks like this -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722020001445/137a6a89-b9ca-42aa-b854-31ae8a856bd4.png" alt class="image--center mx-auto" /></p>
<p>If you run this on a Cloud VPS that has a domain name pointed to it, with the domain in <code>Caddyfile</code>, caddy will provision a certificate from Let's Encrypt.</p>
<h3 id="heading-request-header-manipulation">Request Header Manipulation</h3>
<p>To do request header manipulation just like the Nginx config, there is a directive in caddy called <code>header_up</code> that adds/edits upstream request headers. The complete configuration after header manipulation for localhost looks like this -</p>
<pre><code class="lang-plaintext">localhost

reverse_proxy http://localhost:8080 {
    header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
    header_up X-Real-IP {http.request.remote.host}
    header_up Host {http.request.host}
    header_up X-Forwarded-Proto {http.request.scheme}
}
</code></pre>
<p>And for a domain that you own -</p>
<pre><code class="lang-plaintext">snehangshu.dev

reverse_proxy http://localhost:8080 {
    header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
    header_up X-Real-IP {http.request.remote.host}
    header_up Host {http.request.host}
    header_up X-Forwarded-Proto {http.request.scheme}
}
</code></pre>
<h3 id="heading-production-deployment">Production Deployment</h3>
<p>It is recommended to run caddy in production with their official <a target="_blank" href="https://github.com/caddyserver/dist/blob/master/init/caddy.service">systemd Unit file</a>(s).</p>
<p>In the official unit file, the default location of the <code>Caddyfile</code> is <code>/etc/caddy/Caddyfile</code>. So you need to insert your change to that file or edit the official unit file with your <code>Caddyfile</code> location to enable your configuration. Once the configuration is done, start/restart Caddy using systemctl -</p>
<pre><code class="lang-plaintext">$ sudo systemctl restart caddy.service
</code></pre>
<h3 id="heading-conclusion">Conclusion</h3>
<p>Caddy offers a streamlined and efficient way to set up a reverse proxy with HTTPS, making it an excellent alternative to Nginx for many use cases. Its automatic HTTPS feature, simple configuration syntax, and built-in static file server significantly reduce the complexity and effort required to manage web servers.</p>
<p>While Nginx remains a robust and industry-standard tool, Caddy's modern features and ease of use make it a compelling choice for developers looking for a hassle-free setup. Ultimately, the choice between Nginx and Caddy depends on your specific needs and preferences, but Caddy certainly stands out for its user-friendly approach and powerful capabilities.</p>
<p>Thank you for reading, and if you found this post helpful, please give it a 👍, ask any questions in the comments, and share it with others who might benefit.</p>
]]></content:encoded></item><item><title><![CDATA[Supercharge your home cloud with Umbrel OS and Raspberry Pi]]></title><description><![CDATA[The Cloud is just someone else's computer
- Anonymous

We are all familiar with the above sentence. But it does not tell the whole story about modern-day cloud infrastructure and its complexities that we rely on every day. When cloud computing gained...]]></description><link>https://blogs.snehangshu.dev/supercharge-your-home-cloud-with-umbrel-os-and-raspberry-pi</link><guid isPermaLink="true">https://blogs.snehangshu.dev/supercharge-your-home-cloud-with-umbrel-os-and-raspberry-pi</guid><category><![CDATA[umbrel]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Docker]]></category><category><![CDATA[containers]]></category><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[NAS storage solutions]]></category><category><![CDATA[nas]]></category><category><![CDATA[nasappliance]]></category><category><![CDATA[hardware]]></category><category><![CDATA[iot]]></category><category><![CDATA[iot project]]></category><category><![CDATA[smart home]]></category><category><![CDATA[Bitcoin]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Snehangshu Bhattacharya]]></dc:creator><pubDate>Sat, 20 Apr 2024 11:34:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712824460612/862db14a-3cc2-43fd-9f3f-d27736fdaf2e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>The Cloud is just someone else's computer</p>
<p>- Anonymous</p>
</blockquote>
<p>We are all familiar with the above sentence. But it does not tell the whole story about modern-day cloud infrastructure and its complexities that we rely on every day. When cloud computing gained traction, it only offered raw compute (e.g. Servers with CPU, RAM and Storage). We call this kind of cloud offering Infrastructure-as-a-Service (IaaS).</p>
<p>Built on top of IaaS, Platform-as-a-Service (PaaS) like managed databases, container hosting platforms emerged. Some popular examples are the GCP App Engine, AWS Elastic Beanstalk, Azure App Service etc.</p>
<p>Finally, we get to the Software-as-a-Service (SaaS) paradigm which is the most used format of Cloud Computing we use daily. Some examples are given below:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Category</strong></td><td>Software</td></tr>
</thead>
<tbody>
<tr>
<td>Email</td><td>Gmail, Outlook</td></tr>
<tr>
<td>Cloud Storage</td><td>Google Drive, Onedrive, Dropbox</td></tr>
<tr>
<td>Social Media</td><td>Meta (Facebook, Instagram), X (formerly Twitter)</td></tr>
<tr>
<td>Music Streaming</td><td>Spotify, YouTube Music, Apple Music (formerly iTunes)</td></tr>
<tr>
<td>Productivity</td><td>Microsoft 365 (Word, Excel, Powerpoint, etc.), Google Workspace (Docs, Sheets, Slides, etc.)</td></tr>
<tr>
<td>Messaging Apps</td><td>Whatsapp, Facebook Messenger, Telegram</td></tr>
<tr>
<td>Video Conferencing</td><td>Google Meet, Zoom, Teams</td></tr>
</tbody>
</table>
</div><p>As we can see, we are surrounded by Cloud-based SaaS software and it is challenging to imagine modern life without them. In this blog, we will see how we can replace the impact of some of these services or improve how we use software with a low-cost home cloud solution. But first, we need to talk about network-attached storage or NAS devices.</p>
<h2 id="heading-what-is-a-nas">What is a NAS?</h2>
<p>Network Attached Storage or NAS is a device on your network that acts like a big shared storage device. NAS devices are very similar to desktop PCs, they contain a CPU, RAM, Storage and very good networking hardware, but they do not have a display attached - they are accessed via Web UI, SSH, NFS or SMB. Commercial dedicated NAS devices have 4+ hard drive bays and network support ranging from 2.5 GBps to 10 GBps. These devices usually have hardware RAID chips that offer data sharding, redundancy and failure recovery. The main idea of a NAS is mostly to offload your storage needs from personal devices to a dedicated and always online device. You can store documents, music, photos, 4K movies, local file backups, etc. in a NAS.</p>
<h3 id="heading-how-can-you-assemble-a-nas-device">How can you assemble a NAS device?</h3>
<p>As I mentioned before, NAS devices are desktop computers with some special power. If you assemble a similar desktop it can act as a NAS with some special OS.</p>
<p>Some of these open-source OSes are -</p>
<ol>
<li><p>TrueNAS</p>
</li>
<li><p>OpenMediaVault</p>
</li>
<li><p>Rockstor</p>
</li>
</ol>
<p>Apart from file storage, these OSes offer some PaaS and IaaS services. They can host container platforms and VMs with various levels of difficulty which depends on that particular OS.</p>
<h3 id="heading-is-assembling-or-purchasing-a-nas-worth-it">Is assembling or purchasing a NAS worth it?</h3>
<p>While the OSes that I mention abstract away a lot of underlying complexities, purchasing and maintaining a NAS device require low to high domain expertise depending on the application. If not configured correctly, it can leave your data vulnerable to cyber attacks (if your NAS is connected to the internet), or data loss due to drive failure can be very costly for you or your business.</p>
<p>Also, NAS devices do not make much sense in those countries (or regions) where cloud storage and internet costs are low. I live in India 🇮🇳, and here Internet is one of the cheapest in the world. So people prefer paying for Google One, iCloud or DropBox subscriptions rather than buying a NAS.</p>
<p>In my opinion, if you live in a region where the internet is expensive or slow, or you have a specific business need, then a NAS device makes sense for you. If you are a developer or a tinkerer, build your own NAS, otherwise, invest in a readymade one from any reputed brand like Synology or Asustor.</p>
<h3 id="heading-what-if-nas-devices-offer-some-saas">What if NAS devices offer some SaaS?</h3>
<p>As discussed earlier, the Internet and our daily lives run on various SaaS products. What if there was a simple NAS device that we could build, which is way cheaper than any commercial NAS and does not need much expertise to set up, also can run some SaaS services along with basic file storage? Sounds too good to be true right?</p>
<p>I also did not believe it, until I used Umbrel. Some offering of Umbrel includes <strong>Nextcloud</strong> (self-hosted Google Drive alternative), <strong>Plex</strong> (Cross-platform media server), <strong>Home Assistant</strong> (IoT hub for complete home automation), <strong>Pi-hole</strong> (Network-wide ad blocker), <strong>Photoprism</strong> (Self-hosted photo and video library), <strong>LlamaGPT</strong> (Self-hosted GPT powered by Llama 2) and <strong>Portainer</strong> (Run thousands of apps as a docker container or make your app with a Dockerfile). Also, did I mention that the entire Umbrel platform is open-source, the OS and the apps, everything?</p>
<p>With Umbrel, NAS or Home Cloud suddenly made sense to me! While I made some introduction to Umbrel, let me quickly discuss how I set Umbrel up with a Raspberry Pi and an SSD.</p>
<h2 id="heading-my-humble-umbrel-setup">My Humble Umbrel Setup!</h2>
<p>Since I used parts from past projects, my Umbrel setup cost is different from the current price. I have included a breakdown of the key hardware and their estimated April 2024 price in Indian Rupees (₹) to help you build your own. If you're located outside of India, you can find similar components on your local Amazon or electronics store.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Item</strong></td><td>Cost (including taxes)</td><td>Link</td></tr>
</thead>
<tbody>
<tr>
<td>Raspberry Pi 4 Model B (8 GB RAM)</td><td>7199 ₹</td><td><a target="_blank" href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/?variant=raspberry-pi-4-model-b-8gb">Official retailers</a></td></tr>
<tr>
<td>WD Blue SA510 M.2 250GB</td><td>2759 ₹</td><td><a target="_blank" href="https://amzn.in/d/daDozv3">Amazon</a></td></tr>
<tr>
<td>ORICO Aluminum M.2 NVMe SSD Enclosure (Supports m.2 NVMe and SATA SSD)</td><td>1399 ₹</td><td><a target="_blank" href="https://amzn.in/d/aR84d8s">Amazon</a></td></tr>
<tr>
<td>SanDisk Ultra microSD UHS-I Card 32GB</td><td>369 ₹</td><td><a target="_blank" href="https://amzn.in/d/c0KKV6k">Amazon</a></td></tr>
<tr>
<td>Raspberry Pi 4 Armor Case with Dual Fan</td><td>1259 ₹</td><td><a target="_blank" href="https://amzn.in/d/8Kjs7cS">Amazon</a></td></tr>
<tr>
<td>Ethernet cable</td><td>199 ₹</td><td><a target="_blank" href="https://amzn.in/d/ia4kCGS">Amazon</a></td></tr>
<tr>
<td><strong>Total</strong></td><td>13184 ₹</td></tr>
</tbody>
</table>
</div><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713608040467/5d8fc045-5eaa-4793-9bf3-620ca8e2b041.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-some-insights-about-the-hardware">Some insights about the hardware</h3>
<ol>
<li><p><strong>USB 3.0 Speeds:</strong> Raspberry PI 4 Model B does not support the full 5 Gbps (625 MBps) speed on its USB 3.0 ports because the USB controller chip is connected to the processor via a single PCIe 2.0 x1 lane which supports speeds of 4 Gbps (500 MBps). Any good branded NVMe PCIe 3.0 SSD(Samsung, WD, Crucial etc.) supports speeds up to 3000+ MBps. So if you are trying to build this, do not spend on a fast NVMe SSD if you only intend to use that SSD in a Raspberry Pi project.</p>
<p> SATA SSDs support speeds up to 6 Gbps. You can grab a 2.5-inch SATA SSD / m.2 SATA SSD and one 2.5-inch SATA SSD enclosure / m.2 SATA SSD enclosure which is cheaper. TL;DR SATA SSDs and their enclosures cost less than their NVMe cousins.</p>
</li>
<li><p><strong>USB Cables:</strong> If your SSD enclosure comes with a USB C to USB A cable that supports 5 Gbps or 10 Gbps speeds, you can skip this point. If your SSD enclosure comes with only a USB C to USB C cable with speeds of 5 Gbps or 10 Gbps, you will need one USB C to USB A adapter that supports at least 5 Gbps speed like <a target="_blank" href="https://amzn.in/d/i4hlNfs">this</a>. If you do not like the adapter solution, I suggest you find a good USB C to USB A 5 Gbps cable.</p>
</li>
<li><p><strong>Memory variant of RPi:</strong> Raspberry Pi 4 Model B is available in 4 GB and 8 GB RAM variants. While UmbrelOS is supported for both, it is always better to get 8 GB of RAM for faster performance.</p>
</li>
<li><p><strong>Power Supply:</strong> While the official Raspberry Pi power supply is recommended, you can use any USB-PD compatible mobile charger rated 2.5 A and above.</p>
</li>
</ol>
<h2 id="heading-installing-umbrelos">Installing UmbrelOS</h2>
<p>There is a comprehensive installation guide available on the site of <a target="_blank" href="https://umbrel.com/umbrelos">UmbrelOS</a>. In short, the steps are following -</p>
<ol>
<li><p>Download the UmbrelOS <a target="_blank" href="https://download.umbrel.com/release/1.0.4/umbrelos-pi4.img.zip">image</a> for Raspberry Pi 4.</p>
</li>
<li><p>Download <a target="_blank" href="https://etcher.balena.io/">balenaEtcher</a> to flash the image to the microSD card.</p>
</li>
<li><p>Connect the microSD card to Pi after flashing.</p>
</li>
<li><p>Connect the SSD to any USB 3.0 port (blue port) of your Pi. If you have any data on the SSD please take a backup because it will be erased by UmbrelOS during the setup process.</p>
</li>
<li><p>Connect one end of the Ethernet cable to your home router and another end to the Raspberry Pi. The router must have DHCP enabled for the LAN ports.</p>
</li>
<li><p>Connect the power supply and switch it on.</p>
</li>
<li><p>UmbrelOS will complete the setup within 5 minutes and will be available at <a target="_blank" href="http://umbrel.local/">http://umbrel.local/</a> or <a target="_blank" href="http://umbrel/">http://umbrel</a> on Windows.</p>
</li>
<li><p>Once you enter your name and set your password, you will experience something like this -</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712847927451/69287a92-ef89-47c7-8d46-492bbd6d9fb6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-umbrel-ecosystem">Umbrel Ecosystem</h2>
<h3 id="heading-the-app-store">The App Store</h3>
<p>The app store in Umbrel OS has plenty of apps that cover a lot of ground when it comes to self-hosting applications. The categories range from productivity, media, networking, IoT, developer tools and AI. Here's a sneak peek of the app store -</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/cVO4NRcf1r0">https://youtu.be/cVO4NRcf1r0</a></div>
<p> </p>
<p>You can browse the app store as a web page here: <a target="_blank" href="https://apps.umbrel.com/">https://apps.umbrel.com/</a></p>
<h3 id="heading-my-favourite-apps">My favourite apps</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713035542364/0883e37a-a476-46ae-ae85-ab936b3bf0bd.png" alt="My favourite apps on Umbrel" class="image--center mx-auto" /></p>
<p>Of all of these available apps, some apps I liked and found very useful. I am listing them below -</p>
<ol>
<li><p><strong>Pi-hole:</strong> Pi-hole is a network-wide ad blocker and does not require any browser extensions. It can achieve this by blocking DNS queries for the domains where the ads are served. Just install Pi-hole on your Umbrel and configure your router to use the IP of Umbrel as the primary DNS server. Ads will now get blocked by Pi-hole and you get a squeaky clean web browsing experience.</p>
</li>
<li><p><strong>Gitea:</strong> It is a self-hosted version control service like GitHub, BitBucket or GitLab. Written in Golang, it is very minimal and can easily run on a Raspberry Pi.</p>
</li>
<li><p><strong>qBittorrent:</strong> One of the best open-source torrent clients, totally ad-free. Offload your torrent downloads to Umbrel and transfer them to your device when it is done. This app also comes with an alternative UI of qBitorrent called <a target="_blank" href="https://github.com/VueTorrent/VueTorrent">VueTorrent</a> which is modern, responsive and sleek.</p>
</li>
<li><p><strong>Cloudflare Tunnel:</strong> Connect to your Umbrel device from anywhere using Cloudflare Tunnel. This app runs a very lightweight daemon called <code>cloudflared</code> to maintain a tunnel, you can easily access your Umbrel apps from the Internet.</p>
</li>
<li><p><strong>Home Assistant:</strong> Self-hosted home automation system compatible with thousands of IoT devices and services like Amazon Alexa, Google Assistant, Samsung SmartThings or Apple HomeKit. Privacy first and local control makes it a safer home automation choice than cloud services.</p>
</li>
<li><p><strong>Back that Mac Up:</strong> An extremely easy-to-use SMB or Samba server is made available to your local network by this app. Then it can be mounted and used as a time machine backup location and data can be restored when needed.</p>
</li>
<li><p><strong>File Browser:</strong> A no-nonsense file browser for all your downloads and creations on Umbrel. You can preview basic files, and images, and download them to your machine with this app.</p>
</li>
<li><p><strong>Immich:</strong> Self-hosted Google Photos alternative to store, view, edit, and organize your photos and videos. Comes with a companion mobile app for Android and iOS.</p>
</li>
<li><p><strong>Snapdrop:</strong> An alternative to airdrop for your local network. Open Snapdrop on both devices that you want to transfer in between and transfer files just like Airdrop.</p>
</li>
<li><p><strong>NextCloud:</strong> The most popular self-hosted file storage service. It comes with a beautiful web interface, file-sharing control and many more features. Nextcloud has a cross-platform (Android and iOS) companion app for easy file sync and backup.</p>
</li>
<li><p><strong>Plex and Jellyfin:</strong> These are the best self-hosted media servers found on the Internet. Plex is a user-friendly media server with a wider range of features, client app support, and optional paid extras while Jellyfin is a free, open-source option that prioritizes privacy and gives you more control over your media.</p>
</li>
<li><p><strong>MeTube:</strong> Download videos from YouTube and watch them later. As simple as that.</p>
</li>
<li><p><strong>Chatpad AI:</strong> Alternative UI for ChatGPT with a lot of fine-grained control.</p>
</li>
<li><p><strong>Portainer:</strong> This very powerful container hosting platform is intended for power users who are looking to extend Umbrel's capability. Run docker container, compose stacks and manage, monitor, and modify containers, networks and volumes with this. Using Portainer you can run any web app that can be containerized, on your Umbrel.</p>
</li>
<li><p><strong>LlamaGPT (Special Mention):</strong> Self-hosted LLM on a Raspberry Pi with a ChatGPT-like UI. It takes 5 GB of RAM to run, so it is not suitable to run on the 4 GB RAM variant of the Pi. While great as a proof-of-concept, the generation is very slow (1 word / second) on the Pi 4 8 GB variant.</p>
</li>
<li><p><strong>Crypto / Blockchain / Web3 Apps (Special Mention):</strong> I have avoided listing blockchain-driven apps because I have very limited knowledge of these technologies and have not tested any apps that are available on the Umbrel App Store. If you have used any of these apps, please leave a comment!</p>
</li>
</ol>
<h3 id="heading-all-umbrel-apps-are-containers">All Umbrel apps are containers</h3>
<p>Umbrel is by design, container driven. It means every Umbrel app is a container. As the entire Umbrel software ecosystem is open-source, we can look into one of the most basic Umbrel apps - <a target="_blank" href="https://github.com/getumbrel/umbrel-apps/tree/master/file-browser">File Browser</a>.</p>
<iframe style="width:100%;height:499px" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fgetumbrel%2Fumbrel-apps%2Fblob%2Fmaster%2Ffile-browser%2Fdocker-compose.yml&amp;style=github&amp;type=code&amp;showBorder=on&amp;showLineNumbers=on&amp;showFileMeta=on&amp;showFullPath=on&amp;showCopy=on&amp;fetchFromJsDelivr=on"></iframe>

<p>As we can see, it is not rocket science to run Umbrel apps. If you want to build and run your app on Umbrel, you can read the official documentation for <a target="_blank" href="https://github.com/getumbrel/umbrel-apps/blob/master/README.md">Umbrel App Framework</a>.</p>
<h3 id="heading-umbrel-home">Umbrel Home</h3>
<p><img src="https://umbrel.com/umbrel-open-graph.jpg" alt="Umbrel Home — Your ultimate home server for self-hosting" /></p>
<p>Umbrel sells its own hardware called '<strong>Umbrel Home</strong>' with Umbrel OS preinstalled which has some pretty impressive specifications -</p>
<ol>
<li><p>Intel N100 Quad-core CPU @ 3.4 Ghz</p>
</li>
<li><p>16 GB DDR5 RAM</p>
</li>
<li><p>2 TB SSD</p>
</li>
<li><p>Gigabit Ethernet</p>
</li>
<li><p>3x USB 3.0 ports</p>
</li>
</ol>
<p>You can <a target="_blank" href="https://umbrel.com/umbrel-home">get</a> this one if you are interested in a clean and easy-to-use device than my hacky Raspberry Pi setup!</p>
<h3 id="heading-no-hardware-install">No Hardware Install</h3>
<p>If you do not have any hardware, you can run an <strong>older version(💡)</strong> of Umbrel on a virtual machine to try it out. Install and run a Debian 12 (Bookworm) or Ubuntu 22.04 (Jammy Jellyfish) in a virtual machine on VMWare Workstation, VirtualBox or any cloud platform (kind of beats the purpose of running Umbrel, but it's okay to test it out!).</p>
<p>After the OS is installed, run this command in the terminal and follow the instructions:</p>
<pre><code class="lang-bash">curl -L https://umbrel.sh | bash
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">According to the Umbrel website, the latest version of Umbrel OS will be ready to run on any Debian / Ubuntu device by this month (April 2024).</div>
</div>

<h3 id="heading-limitations">Limitations</h3>
<ol>
<li><p>While very promising, as a new platform, it is not entirely free of bugs, as I have encountered a few while using it.</p>
</li>
<li><p>It does not yet have support for any data redundancy technology like RAID. If you are storing any important data on UmbrelOS, please be careful and keep a backup!</p>
</li>
</ol>
<h2 id="heading-wrapping-it-up">Wrapping it up</h2>
<p>I never had so much fun exploring an OS before (saying as a former Linux distrohopper)! Umbrel brings along a very new and unique way of setting up a home cloud, and with it, we can somewhat beat internet censorship, and tracking, block ads, and offload some data and tasks from our computing devices! I would encourage all of you who own a Raspberry Pi to try Umbrel, tinker with it and share the experience.</p>
<p>If you are still here, thank you for reading! If you like this work of mine, give a like and please let me know your feedback below in the comment section! Also, share it with someone who might learn something from my work!</p>
<p>Shutting down....🤖</p>
]]></content:encoded></item><item><title><![CDATA[Dead Man's Switch style Application monitoring with healthchecks.io]]></title><description><![CDATA[Hello reader, welcome to this blog! I am sorry to start it on a dead-ly note, but rest assured this blog has nothing to do with anything violent. I will be talking about failing app deployments (violently, if it is in production) and how to get an in...]]></description><link>https://blogs.snehangshu.dev/dead-mans-switch-style-application-monitoring-with-healthchecksio</link><guid isPermaLink="true">https://blogs.snehangshu.dev/dead-mans-switch-style-application-monitoring-with-healthchecksio</guid><category><![CDATA[monitoring]]></category><category><![CDATA[Devops]]></category><category><![CDATA[deployment]]></category><category><![CDATA[metrics]]></category><category><![CDATA[Flask Framework]]></category><category><![CDATA[twilio]]></category><category><![CDATA[sendgrid]]></category><category><![CDATA[render.com]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[containers]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Snehangshu Bhattacharya]]></dc:creator><pubDate>Fri, 15 Mar 2024 18:14:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707680371927/68ca7093-92d1-45d9-8d3a-0e11ee51af7e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello reader, welcome to this blog! I am sorry to start it on a dead-ly note, but rest assured this blog has nothing to do with anything violent. I will be talking about failing app deployments (violently, if it is in production) and how to get an instant notification when they fail.</p>
<h2 id="heading-why-do-app-deployments-fail">Why do app deployments fail?</h2>
<p>Before understanding why apps fail, we must understand what kind of apps we are discussing. By far the most popular kind of apps are web apps that run our beloved internet. Usually, the web apps need to be served by a web server. Here are some reasons why web servers might fail:</p>
<ol>
<li><p>Resource overload, like server dying due to too much traffic, out-of-memory etc.</p>
</li>
<li><p>Cyberattacks, like DDoS (distributed denial-of-service) or other vulnerabilities.</p>
</li>
<li><p>Bugs, which can happen to the best of software.</p>
</li>
<li><p>Server component failures, power outages or any natural causes that can impact a data centre where the server is hosted.</p>
</li>
</ol>
<h2 id="heading-dead-man-what">Dead man what?</h2>
<p>Dead Man's Switch, existed long before computers were born. Quoting Wikipedia, here goes the definition:</p>
<blockquote>
<p>A dead man's switch is a switch that is designed to be activated or deactivated if the human operator becomes incapacitated, such as through death, loss of consciousness, or being bodily removed from control. Originally applied to switches on a vehicle or machine, it has since come to be used to describe other intangible uses, as in computer software.</p>
</blockquote>
<p>Focusing on the last line of the definition, a software use of Dead Man's Switch should work like this in our server failure scenario:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">😵</div>
<div data-node-type="callout-text">When the server fails, flip the switch</div>
</div>

<p>Now, how to detect if the server has failed or not? There are two ways to do that -</p>
<ol>
<li><p><strong>Heartbeat:</strong> Our server sends a signal (say an HTTP request) to a monitoring service in periodic intervals.</p>
</li>
<li><p><strong>Polling:</strong> A monitoring service sends a request to our server to check if it's alive in periodic intervals.</p>
</li>
</ol>
<p>In this blog, we are going ahead with Heartbeat. So, when there is no heartbeat, we consider the server dead and flip the switch to do something about the incident.</p>
<h2 id="heading-our-demo-app">Our demo app</h2>
<p>So here's the plan, we will make a very simple Flask app and implement a library called <code>apscheduler</code> to periodically ping our monitoring service. We will use another library called <code>requests</code> to send a <code>GET</code> request to the monitoring service. Our requirements.txt for this project looks like:</p>
<pre><code class="lang-plaintext">Flask==3.0.2
APScheduler==3.10.4
requests==2.31.0
</code></pre>
<p>Our goal here is not to build a flask app, so I just made an app that gets a random cat image (😸) from an API and renders it.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> apscheduler.schedulers.background <span class="hljs-keyword">import</span> BackgroundScheduler
<span class="hljs-keyword">from</span> apscheduler.triggers.interval <span class="hljs-keyword">import</span> IntervalTrigger
<span class="hljs-keyword">from</span> flask <span class="hljs-keyword">import</span> Flask, render_template_string

app = Flask(__name__)
scheduler = BackgroundScheduler()


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_heartbeat</span>():</span>
    print(<span class="hljs-string">'The cat is alive 😸, heartbeat sent!'</span>)
    requests.get(<span class="hljs-string">'&lt;heartbeat-monitoring-service-url&gt;'</span>)


scheduler.add_job(
    func=send_heartbeat,
    trigger=IntervalTrigger(minutes=<span class="hljs-number">1</span>)
)

scheduler.start()


<span class="hljs-meta">@app.route('/')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hello_world</span>():</span>  <span class="hljs-comment"># put application's code here</span>
    res = requests.get(<span class="hljs-string">'https://api.thecatapi.com/v1/images/search'</span>)
    url = res.json()[<span class="hljs-number">0</span>][<span class="hljs-string">'url'</span>]
    width = res.json()[<span class="hljs-number">0</span>][<span class="hljs-string">'width'</span>]
    height = res.json()[<span class="hljs-number">0</span>][<span class="hljs-string">'height'</span>]
    <span class="hljs-keyword">return</span> render_template_string(<span class="hljs-string">f'&lt;img src="<span class="hljs-subst">{url}</span>" width="<span class="hljs-subst">{width}</span>" height="<span class="hljs-subst">{height}</span>"&gt;&lt;/img&gt;'</span>)


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    app.run(debug=<span class="hljs-literal">True</span>)
</code></pre>
<p>In the above code, <code>BackgroundScheduler</code> class is used to run a separate thread in our flask app, which pings our monitoring service. To set the schedule of the heartbeat request <code>IntervalScheduler</code> is used. I have used 1-minute intervals for practical reasons. You can change the interval ranging from seconds to weeks as per your requirement.</p>
<p>Let's run our app:</p>
<pre><code class="lang-bash">$ python app.py
</code></pre>
<p>Finally, our app looks like this when run locally -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707989146039/74af7022-63e5-4a88-9be1-d3fefc9a2be4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-our-monitoring-service-healthchecksiohttpshealthchecksio">Our monitoring service: <a target="_blank" href="https://healthchecks.io/">healthchecks.io</a></h2>
<p>healthchecks.io is a great open-source tool for monitoring cron/background jobs and has a lot of integrations built in to notify us via email, webhook, SMS, phone calls, discord, slack, telegram, WhatsApp, teams and many more ways. They have a fully managed service that charges around 5$ / month. But as it is <a target="_blank" href="https://github.com/healthchecks/healthchecks">open-source</a>, we can easily self-host it for almost free (like free-tier VMs in Google Cloud). I will be showing how to send alerts if our app is down via emails, calls, and SMS.</p>
<p>Before we get started with configuring healthchecks.io, we need to configure another service that will help us route our emails, calls, SMSes and WhatsApp messages.</p>
<h2 id="heading-sendgrid-and-twilio">Sendgrid and Twilio</h2>
<p>We will be using SendGrid for sending our emails and Twilio for calling, sending SMS and WhatsApp messages.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">SendGrid was acquired by Twilio in 2019, so we are technically two different services from the same company!</div>
</div>

<h3 id="heading-sendgrid">Sendgrid</h3>
<p>Sendgrid is a very popular email delivery platform that helps businesses send both marketing and transactional emails with features like email templates, tracking and analytics. We would not need all of these features, just mail-sending will do!</p>
<p>Go to <a target="_blank" href="https://sendgrid.com/en-us/solutions/email-api">https://sendgrid.com/en-us/solutions/email-api</a> and sign up, after finishing all the steps, you'll be looking at a page like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708191196501/8c66a710-678a-47e7-8471-17f52d0b134f.png" alt class="image--center mx-auto" /></p>
<p>Click <code>Create sender identity</code> button, you will be presented with a form like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708192069969/9cd10477-6762-45e4-83dc-aba4ca777c12.png" alt class="image--center mx-auto" /></p>
<p>As this is only a demo, I have filled in only the necessary details. You can use your own gmail address as from email address and reply-to address. After you click create, a verification mail will be sent to your gmail inbox, follow the instructions to verify your email.</p>
<p>Once you are done, go back to <a target="_blank" href="https://app.sendgrid.com/">https://app.sendgrid.com/</a> and click Email API -&gt; Integration Guide. Choose the option SMTP Relay.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708192449020/fd639b79-bf18-4419-91dd-012185cee13b.png" alt class="image--center mx-auto" /></p>
<p>Now, give a name to your API key and create one. Once the API key is created, save all of the configuration options somewhere safe. We will use these later.</p>
<h3 id="heading-twilio">Twilio</h3>
<p>Twilio is a cloud communications platform that equips developers with the tools to build communication features directly into their applications. These features can include things like - SMS, calls, chat and video conferencing applications.</p>
<p>Twilio provides the building blocks for developers to create a variety of communication experiences within their software. They accomplish this through a set of Application Programming Interfaces (APIs) that can be integrated into the developer's code. We will be using these powerful APIs of Twilio from our healthchecks.io deployment. But first, we need to sign up for a Twilio account.</p>
<p>Go to <a target="_blank" href="https://www.twilio.com/try-twilio">https://www.twilio.com/try-twilio</a> and sign up with your email, you can also sign up faster using your Google account. After this step, Twilio will ask you to verify your phone number (this verification is mandatory). You can either verify via an SMS or a voice call.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710057041845/77157c08-dd37-4466-9bef-21024d97b485.png" alt class="image--center mx-auto" /></p>
<p>After the verification is complete, you will get a recovery code, please save it somewhere safe!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710057730425/678d7888-78d1-413b-accf-8372425adfc7.png" alt class="image--center mx-auto" /></p>
<p>Next, create your account, give it a name and click verify to start your trial.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Twilio might ask to verify your phone multiple times!</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710058076431/d66e78f9-1b78-4b5d-974b-5b22aa4b795c.png" alt class="image--center mx-auto" /></p>
<p>Once the account creation and verification are completed, you will be redirected to the Twilio console and there you will be asked to get a Twilio phone number. This step is mandatory for sending SMS and dial numbers. Click get a phone number and Twilio will assign a random US phone number to you from their pool of numbers.</p>
<p>Once you get a number, scroll down the console homepage and you will see the <code>Account Info</code> section with <code>Account SID</code>, <code>Auth Token</code> and <code>My Twilio phone number</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710059021534/5a9a02e7-b2f7-45df-9154-f4062d64dfce.png" alt class="image--center mx-auto" /></p>
<p>This is the information that we need to make calls and send SMS from healthchecks.io.</p>
<p>One last caveat, as this Twilio account is a trial account with no payment methods, we get credits of <strong>$ 15.50</strong>, but we can only make calls and send messages to the numbers that we verify. To verify a number, navigate to <code>Phone Numbers -&gt; Manage -&gt; Verified Caller IDs</code> and verify the number(s) that you want to receive a call/SMS on. Your own number (the number that you verified your Twilio account with) will already be added to the verified caller IDs.</p>
<h2 id="heading-deploying-healthchecksio">Deploying healthchecks.io</h2>
<p>Healthchecks.io is available as an open-source project on GitHub: <a target="_blank" href="https://github.com/healthchecks/healthchecks">https://github.com/healthchecks/healthchecks</a> as mentioned earlier. It is built using a very popular Python web framework - <a target="_blank" href="https://www.djangoproject.com/">Django</a>. If you are not familiar with Django, it's fine, because setting up healthchecks does not need any Django expertise. The basic setup procedure is described in the readme.</p>
<p>To do this deployment we will be using two things:</p>
<ol>
<li><p>A docker image of healthchecks (already available officially at <a target="_blank" href="https://hub.docker.com/r/healthchecks/healthchecks">DockerHub</a>)</p>
</li>
<li><p>A PostgreSQL database instance for the database.</p>
</li>
</ol>
<p>I will be using a service called <a target="_blank" href="https://render.com/"><strong>render.com</strong></a> to host the healthchecks service which is a cloud platform that supports rapid development. It offers features to streamline the process of building, deploying, and scaling applications. It comes with a generous free tier to run under-development applications easily.</p>
<p>To get started, visit <a target="_blank" href="https://render.com/"><strong>render.com</strong></a> and sign up for an account. Then follow these steps -</p>
<h3 id="heading-create-a-postgresql-database">Create a PostgreSQL database</h3>
<ol>
<li><p>Click on <code>New +</code> and then click <code>PostgreSQL</code>.</p>
</li>
<li><p>Give a unique name to your instance, select a region that is closest to you (like I chose Singapore as I live in India), choose the instance type as <code>Free</code> (unless you want to pay and are doing a production deployment), and leave all the other options as default.</p>
</li>
<li><p>Click on <code>Create Database</code>.</p>
</li>
<li><p>After the database is created, on the detail page there will be a connections section. We will need some of these pieces of information to connect to the db from our healthchecks container.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710425980344/b07a5fc5-10fe-4e65-93fd-b5641cdd459b.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-deploy-the-docker-image-of-healthchecks">Deploy the docker image of healthchecks</h3>
<ol>
<li><p>On the render.com dashboard, click on <code>New +</code> and then click <code>Web Service</code> .</p>
</li>
<li><p>From the next page, choose the option <code>Deploy an existing image from a registry</code>.</p>
</li>
<li><p>Enter the Image URL as <code>healthchecks/healthchecks</code> (mentioned in the DockerHub page) and click next.</p>
</li>
<li><p>Now give a unique name, choose the closest region and Instance type as <code>Free</code>.</p>
</li>
<li><p>There is a section called <code>Environment Variables</code> where we need to set a lot of configuration parameters for our healthchecks instance. The variables come as key-value pairs and are explained in the following table.</p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Key</td><td>Value</td><td>Remarks</td></tr>
</thead>
<tbody>
<tr>
<td>ALLOWED_HOSTS</td><td>*</td><td><code>Who can access our healthchecks site</code></td></tr>
<tr>
<td>DB</td><td>postgres</td><td><code>Database type</code></td></tr>
<tr>
<td>DB_HOST</td><td><code>&lt;postgres_db_hostname&gt;</code></td><td><code>DB hostname found in postgres service created previously</code></td></tr>
<tr>
<td>DB_NAME</td><td><code>&lt;postgres_db_name&gt;</code></td><td><code>DB name found in postgres service created previously</code></td></tr>
<tr>
<td>DB_PASSWORD</td><td><code>&lt;postgres_db_password&gt;</code></td><td><code>DB password found in postgres service created previously</code></td></tr>
<tr>
<td>DB_PORT</td><td><code>&lt;postgres_db_port&gt;</code></td><td><code>Postgres database port, by default 5432</code></td></tr>
<tr>
<td>DB_USER</td><td><code>&lt;postgres_db_user&gt;</code></td><td><code>DB user found in postgres service created previously</code></td></tr>
<tr>
<td>DEBUG</td><td>False</td><td><code>Django debug flag, keep it True until the deployment works correctly</code></td></tr>
<tr>
<td>DEFAULT_FROM_EMAIL</td><td><code>&lt;sendgrid_sender_identity_email&gt;</code></td><td><code>Sender identity email id from sendgrid</code></td></tr>
<tr>
<td>EMAIL_HOST</td><td>smtp.sendgrid.net</td><td><code>E-mail server hostname</code></td></tr>
<tr>
<td>EMAIL_HOST_PASSWORD</td><td><code>&lt;sendgrid_api_key&gt;</code></td><td><code>E-mail server authentication password</code></td></tr>
<tr>
<td>EMAIL_HOST_USER</td><td>apikey</td><td><code>E-mail server authentication username</code></td></tr>
<tr>
<td>EMAIL_PORT</td><td>587</td><td><code>E-mail server port, e.g. 25 (SMTP), 587 (TLS) and 465 (SSL)</code></td></tr>
<tr>
<td>EMAIL_USE_TLS</td><td>True</td><td><code>Force enable TLS</code></td></tr>
<tr>
<td>EMAIL_USE_VERIFICATION</td><td>True</td><td><code>Enable email verification</code></td></tr>
<tr>
<td>PORT</td><td>8000</td><td><code>Healthchecks container port</code></td></tr>
<tr>
<td>SECRET_KEY</td><td><code>&lt;unique_unpredictable_key&gt;</code></td><td><code>Used by Django to do any cryptographic operations, read</code><a target="_blank" href="https://docs.djangoproject.com/en/5.0/ref/settings/#secret-key"><code>more</code></a> 💡</td></tr>
<tr>
<td>SITE_NAME</td><td>Dead Man Switch</td><td><code>Unique name of your site</code></td></tr>
<tr>
<td>SITE_ROOT</td><td></td><td><code>Keep it blank for 1st time, more details later</code> ⚡️</td></tr>
<tr>
<td>TWILIO_ACCOUNT</td><td><code>&lt;twilio_account_sid&gt;</code></td><td><code>Twilio account SID from Twilio dashboard</code></td></tr>
<tr>
<td>TWILIO_AUTH</td><td><code>&lt;twilio_auth_token&gt;</code></td><td><code>Twilio account auth token from Twilio dashboard</code></td></tr>
<tr>
<td>TWILIO_FROM</td><td><code>&lt;twilio_phone_number&gt;</code></td><td><code>Twilio allocated phone number for caller ID and sms</code></td></tr>
</tbody>
</table>
</div><div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can generate a <code>SECRET_KEY</code> using this <a target="_blank" href="https://djecrety.ir/">site</a>.</div>
</div>

<ol start="6">
<li><p>After filling in all the environment variables, click <code>Create Web Service</code>. The service will be deployed within a few minutes.</p>
</li>
<li><p>After the deployment is done, on the service page you will find the service URL that looks like this: <code>https://&lt;service_name&gt;-&lt;random_string&gt;.onrender.com</code></p>
</li>
<li><p>Copy the URL, and then go to the <code>Environment</code> tab of the left sidebar and paste it as the value of <code>SITE_ROOT</code>. Click <code>Save Changes</code> to save and restart the deployment.</p>
</li>
<li><p>Once the deployment is done, our healthchecks site will be up at the URL.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710439444552/9cdce7ae-db9c-4f51-bf21-30dcca7d83f3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-sign-up-and-set-up-healthchecks">Sign up and set up healthchecks</h2>
<ol>
<li><p>Click <code>Sign Up</code> on the top right corner of our healthchecks site, enter your email address and click <code>Email me a link</code>. You will receive a login link in your email.</p>
</li>
<li><p>Once you click the login link, you will be directed to the home page that looks like this below:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710440386243/b401ae2c-b9fe-4f6e-b606-f22e0370f97d.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>Now that we are ready with a working deployment, let's proceed with the call and SMS notifications.</li>
</ol>
<h3 id="heading-how-checks-work-in-healthchecksio">How checks work in healthchecks.io</h3>
<ol>
<li><p>You register a check in the dashboard, and you get a ping URL.</p>
</li>
<li><p>Every new check is marked as <code>inactive</code> at the start.</p>
</li>
<li><p>Every check has a period and grace time setting.</p>
</li>
<li><p>You can set the period via simple <strong>interval</strong>, <strong>cron string</strong> or <strong>onCalender</strong> expressions.</p>
</li>
<li><p>You need to configure your app (that you want to monitor) to send an HTTP HEAD/GET/POST/PUT request to the ping URL according to the interval. Once a check gets a ping, its status is marked as <code>up</code>.</p>
</li>
<li><p>If healthchecks does not receive the ping at the specified interval, the check is marked <code>delayed</code> and the grace time counter starts.</p>
</li>
<li><p>If there is no ping even within the grace time, healthchecks marks the check as <code>down</code> .</p>
</li>
<li><p>As soon as the check is marked <code>down</code>, healthchecks send notifications to all registered integrations associated with the check.</p>
</li>
</ol>
<h3 id="heading-call-notifications">Call notifications</h3>
<p>Call notifications use Twilio's API, which is built in with healthchecks. The idea is simple: call the specified number and play a predefined message when a check goes down. The steps are -</p>
<ol>
<li><p>Go to <code>Integrations</code> menu and find <code>Phone Call</code> and click it's <code>Add Integration</code> button.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710442082969/1299615e-dbdd-46fa-a17a-d815a3d3773c.png" alt class="image--center mx-auto" /></p>
<p> Add a Label and your phone number where you want to receive the alert call and save the integration.</p>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">💡</div>
 <div data-node-type="callout-text">Note: As the Twilio account is a trial account, you must add a verified caller ID (phone number) here. Otherwise, this integration would not work.</div>
 </div>
</li>
<li><p>Send a test notification From the integrations list to check if the integration works. You will receive a call within a minute if everything is working.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710442216792/56d78dbf-ee5d-4f3b-a2ba-ed27e3df3bda.png" alt class="image--center mx-auto" /></p>
<p>The audio sample for this test is given below:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://soundcloud.com/edrake009/audio-hc-twl-test-msg/s-KkmcEnFtvkb?si=5ff7fe8337cc4ffe9692f95ecb1a4e40&amp;utm_source=clipboard&amp;utm_medium=text&amp;utm_campaign=social_sharing">https://soundcloud.com/edrake009/audio-hc-twl-test-msg/s-KkmcEnFtvkb?si=5ff7fe8337cc4ffe9692f95ecb1a4e40&amp;utm_source=clipboard&amp;utm_medium=text&amp;utm_campaign=social_sharing</a></div>
<p> </p>
<h3 id="heading-sms-notifications">SMS Notifications</h3>
<p>SMS Notifications are very similar to phone calls as they also use Twilio APIs. To enable SMS notifications follow the steps -</p>
<ol>
<li><p>In the <code>Integrations</code> menu find <code>SMS</code> and click it's <code>Add Integration</code> button.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710520200737/d4314184-7886-4a56-994f-b3285a3937d8.png" alt class="image--center mx-auto" /></p>
<p> Add a label, your phone number, and check when you want a notification. Remember to verify your number if you are using a Twilio trial account.</p>
</li>
<li><p>Send a test notification and verify the integration. A sample notification looks like this:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710521051625/c7ae5265-7259-4b09-bff5-02c0ed5efb9d.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h3 id="heading-email-notifications">Email Notifications</h3>
<p>The most basic integration, i.e. email should be already added as a sample where you get notifications in the email that you use to sign up. You can more email integrations if you want to send email notifications to more people.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710525022838/d3077a23-8a70-4947-bfab-c4c08fd4f55d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Phew! That was a very long blog. If you are still here, thank you for reading!</p>
<h3 id="heading-important-takeaways-for-deployment">Important takeaways for deployment</h3>
<p>I have made a very quick and easy deployment of healthchecks which is never recommended for production use. The free tier of render.com is very limited where web services spin down when inactive and Postgres instances are deleted after 90 days. You should consider deploying healthchecks in a properly designed fault-tolerant VM or container to use it in production.</p>
<h3 id="heading-arent-you-forgetting-something">Aren't you forgetting something?</h3>
<p>Remember our cat image generator app? You can now plug in the ping URL to this function below and get a health check every minute!</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_heartbeat</span>():</span>
    print(<span class="hljs-string">'The cat is alive 😸, heartbeat sent!'</span>)
    requests.get(<span class="hljs-string">'&lt;heartbeat-monitoring-service-url&gt;'</span>)
</code></pre>
<p>Healthcheck will send a notification via call and SMS when your app goes down.</p>
<p>Please let me know in the comments how you are implementing your own use cases of heathchecks (my favourite use case is monitoring cron jobs that fail silently). If you like this post, please drop a like to this blog, and share it with anyone who might get some benefit out of it.</p>
<p>Signing off 😴.</p>
]]></content:encoded></item></channel></rss>