Skip to main content

Command Palette

Search for a command to run...

How I Reduced Infra Costs by 50% Using GitHub Actions

Updated
How I Reduced Infra Costs by 50% Using GitHub Actions

Building a full stack web app is fun. Deploying it? Not so much. And what good are those projects that never make it out of localhost. While frontend deployment for small side projects is pretty much sorted, thanks to generous free tiers of vercel, managing backend deployments are still complicated. Providers like render, and railway have small free tier limits or cold start issues affecting latencies. Deployment costs on cloud providers like amazon’s AWS or Microsoft’s Azure starts from around $10 monthly. Github Actions - a serverless solution - can bring those costs down to almost zero if it fits in your use case. This blog covers how I refactored my project from a web app to a serverless function, saving massively on deployment costs.

Before We Begin

I recently built a personal journaling self help tool - JurnAI. Its core functionality was a journal fetching and processing pipeline that used to run every morning at a fixed time. It was responsible for processing journals from last night and trigger mails to users after processing the content. Besides this functionality, the web server also supported new user signup flows. The current backend comprised of a python app built on FastAPI. The frontend app was connected to this server for handling user signups. The daily journal processing also used to run on this server, triggered by a cron job every morning. Besides that, there were some additional cron based flows that used to run at regular intervals e.g - deactivating inactive users, and sending reminder mails to churning users. Inshort, apart from signups, there was no other flow that required the server to be active all the time. However, when deploying this web app on cloud, we need to take into account all the flows when deciding how much computation power is needed. For e.g. we need a server that stays active 24x7 for user signups. However we also need the server to be powerful enough (memory and CPU) to handle daily journal processing as it involves multiple concurrent processing for all the active users. Here is a snapshot of the current backend structure.

As a result, my monthly estimated costs for running this server came at around $12 per month on Azure. Thanks to my leftover student credits, I did not care much. However, once the credits depleted and my project went down, I came to realize the cost of my running server.

I wanted to keep running this project [for myself]. So, I decided to explore options to keep running it at minimal costs. Creating new accounts for free credits is something that is not worth the efforts / unethical in some sense. As I dig deeper, I came to know about serverless deployments. So the problem is, I have built a web app with non-uniform incoming traffic and resources usage. My signup flows requires the server to be active all the time, however my daily processing pipeline needs a slightly powerful machine for a fixed duration to handle computation.

Serverless - Stops You From Becoming Money Less

What if I could deploy my web app in two places. The first server will solely be responsible for handling signups. It will stay active 24x7 but will be a very small machine, since the signups don’t consume that much of computation resources for me. The second server will handle all the data processing pipelines. I will use a slightly powerful machine to account for the processing needed. Additionally, since I don’t need this machine to be active all the time, I can just stop this instance when not in use. As the instances are charged per hour, the cost savings can be huge. For e.g running a t3.micro instance on AWS costs $0.0112 per hour. Running it for 24 hours a day will cost ~$8.18 monthly. However running the same instance 1 hour daily, will cost ~$0.34 per month. A massive 96% savings in monthly cost. Now obviously, It is practically not feasible to start / stop instances manually everyday. This is where I decided to use Github Actions - an automation offering to run scheduled workflows.

Note: While there are ways to automate the instance to start and stop, it comes with added costs and various other nuances which are not being discussed in this blog.

Moving to Github Actions

I cannot run my code the same way on a serverless infra, like I was running on my current server. In simple words, we can’t deploy web apps directly. The whole concept of serverless revolves around functions. Think of your code in terms of functions. You can deploy all the code required for running those functions and then create a script [workflow] to trigger those functions. So technically speaking, I don’t need to run my FastAPI app. Instead, I can extract the logic in wrapper functions and call those functions directly. Confusing? Let’s understand with an example.

Say you have a web app. In that you have an endpoint responsible for deactivating inactive users. The endpoint calls an internal method that handles the business logic. In a traditional setting, to trigger this flow, you need to first run the python app, then hit the endpoint.

from flask import Flask, jsonify
from shared.database import init_db
from shared.business_logic import deactivate_inactive_users

app = Flask(__name__)

# 1. Initialize DB when the web server starts
init_db()

@app.route('/admin/deactivate-users', methods=['POST'])
def api_deactivate_users():
    """
    Triggered by an Admin clicking a button or an external API call.
    """
    try:
        # Call the shared business logic
        result = deactivate_inactive_users()
        return jsonify(result), 200
    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == "__main__":
    app.run(port=5000)

In serverless context, you can think of this endpoint as a function. To handle the same functionality, you need to call the deactivateUsers() function. You can define this function separately in another script. Both the endpoint and your function still calls the same business logic. Only thing that has changed is how you call them.

import sys
import os

# Add the parent directory to sys.path so we can import 'shared'
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

from shared.database import init_db
from shared.business_logic import deactivate_inactive_users

def main():
    print("🚀 Starting Batch Job: User Deactivation")

    # STEP 1: Manual Initialization (The part the Web App usually handles)
    # We must explicitly load env vars and connect to DB here.
    try:
        init_db()
    except Exception as e:
        print(f"❌ Critical Error: Could not connect to DB. {e}")
        sys.exit(1)

    # STEP 2: Execute the Business Logic
    try:
        result = deactivate_inactive_users(days_inactive=30)
        print(f"🏁 Job Finished Successfully: {result}")
    except Exception as e:
        print(f"❌ Error during execution: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Since you are not initialising the web app, hence you need to take care of the processes that are taken care of while starting the server - initialising database, and loggers separately. Again you can just bundle this logic in a single script and call it before executing your main function. Not a big issue.

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Run Deactivation Script
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        # Simply run the script, no need to start the flask server
        run: python scripts/run_cleanup.py

After making the move to serverless, I had refactored my web app. While my signup flows are called from the web server context, all my processing pipelines are encompassed in a single script. This script is executed accordingly via the configured github action workflows.

/my-project
├── app.py                  # The Web Server (FastAPI)
├── scripts/
│   └── run_cleanup.py      # The Standalone Script (For GitHub Actions)
└── shared/
    ├── __init__.py
    ├── database.py         # DB Connection logic
    └── business_logic.py   # The actual "Deactivate Users" logic

How much scale can it handle?

Your server used to run 24x7 giving you complete visibility into resource utilization. How much will it cost to run it now? What if serverless is more expensive in the long run? If you are thinking along these lines, then you already my friend. Let’s do a cost analysis. Serverless infrastructure charges you for the total minutes you use the server for. In my current infrastructure, the daily content processing pipeline runs for about 20 seconds on an average to process about 100 concurrent users every day. So to handle processing for around 10k users, we require around 2k seconds daily or ~1000 minutes monthly. Using a standard github hosted runner (ubuntu - linux 2-core cpu machine), it will cost around 6$. If you have a public repository, github actions are free of cost. With the only limitation that you can’t run jobs longer than 6 hours. So basically, the entire cost comes down to literally zero for your side projects. Incase you don’t want to make your code public, private repositories also gets around 2k minutes monthly of free usage. compare that to 12$ of monthly usage in fixed server costs.

Note: With the above usage, you may also fall under free tier of AWS Lambda as well.

But What About Signup Flow?

Github Actions does not support invocation via HTTP triggers. However other cloud service providers like AWS Lambda and Microsoft Azure Functions supports them. This is because actions are supposed to run project’s automation tasks like running builds, test suites and not built for true serverless computing. However Lambda and Functions are truly serverless. What that means is that you can invoke these serverless functions by calling a specific endpoint, similar to how you call an API of your hosted server. But should you do it? Maybe not. Serverless functions have a cold-start issue - the time it takes to warm up before it actually starts executing your function. This latency is manageable for background tasks. However, for client-interactions like signups, using this does not make much sense to me. So, you will still need a small lightweight server to handle client-side interacted flows. But, since your heavy processing is now on serverless infra, you can consider opting for a lightweight machine. For e.g railway’s free tier usage of 1$ monthly will be able to help you out. Additionally, you can also take their hobby plan (5$ monthly) and use that limit shared between multiple projects.

Should You Move Your Infra To Serverless?

Serverless is not some discount coupon that you can apply to your backend infrastructure to cut down deployment costs. It is a smart and cost efficient utilisation of infrastructure for use-cases involving fixed usage patterns dedicated to specific time intervals everyday. If it does not make sense to your use case - don’t.

Problems I faced while using Github Actions

It was not a smooth ride. There are some scenarios that you should be aware of before using github actions for your workflows.

Inconsistent Timings

My Workflow was supposed to run at sharp 8 AM in the morning every day. What I have observed on standard github runners is that there is always some delay in running my workflows. Sometimes around 10 minutes, other days upto 60 minutes of delay. This can happen during high traffic scenarios where multiple user flows fight for execution slot. But this delay is consistent for some reasons. For e.g, one of my workflow runs with a delay of 10 minutes consistently. so, you can tweak your scheduled timings accordingly.

Cold Start Latency

Though it is a problem with every serverless solution. For Lambda, it is a nominal 1-2 seconds of delay. Whereas for github actions using standard hosted runners, it can be as high as 20-30 seconds for setting the initial containers. However, I did not face major issue with github actions as such regarding this.

Final Verdict

As an engineer, never stop exploring. Few decades ago, people used to run and maintain their own physical servers. Then came cloud providers making deployments simplified. With the rise of companies like vercel, railway and render, deployments literally went one code push away. Serverless compute will contribute in their own way for powering agentic asynchronous workflows. Optimize for learning, not for costs if you are building projects. However, there’s no doubt that these serverless solutions significantly bring down the overall deployment costs for low to moderate traffic projects, if used correctly. That’s it for this blog. I hope you enjoyed reading this deep dive about my infra-adventures. Do you want me to cover a step by step tutorial of hosting a project on github actions? Let me know in the comments below. Until next time! Namaste.