<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: R.A. Olanrewaju</title>
    <description>The latest articles on DEV Community by R.A. Olanrewaju (@jpegcreate).</description>
    <link>https://dev.to/jpegcreate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3812977%2F06202522-f800-408e-a267-d1fd63e78510.jpeg</url>
      <title>DEV Community: R.A. Olanrewaju</title>
      <link>https://dev.to/jpegcreate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jpegcreate"/>
    <language>en</language>
    <item>
      <title>I built a serverless agent that finds open-source issues for me every morning</title>
      <dc:creator>R.A. Olanrewaju</dc:creator>
      <pubDate>Mon, 22 Jun 2026 08:57:11 +0000</pubDate>
      <link>https://dev.to/jpegcreate/i-built-a-serverless-agent-that-finds-open-source-issues-for-me-every-morning-43og</link>
      <guid>https://dev.to/jpegcreate/i-built-a-serverless-agent-that-finds-open-source-issues-for-me-every-morning-43og</guid>
      <description>&lt;p&gt;I contribute to OWASP Nest. Before that I was spending 20-30 minutes manually browsing GitHub for issues that matched my stack, most of which turned out to be stale, already claimed, or just not a good fit. I got tired of that. So I built something to do it for me.&lt;/p&gt;

&lt;p&gt;The result is OSS-Contrib-Scout: a Lambda function that runs every morning at 8am, searches GitHub for Python issues tagged &lt;code&gt;good-first-issue&lt;/code&gt; or &lt;code&gt;help-wanted&lt;/code&gt;, scores each one against my actual stack using Gemini AI, and pushes a ranked digest to Telegram.&lt;/p&gt;

&lt;p&gt;This post covers how it's built, what broke, and what a clean production run actually looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Three modules, one handler, one schedule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EventBridge Scheduler (daily, 8am UTC)
        │
        ▼
   Lambda Function
        │
        ├── github_search.py   → GitHub Issues Search API
        ├── scorer.py          → Gemini API (scoring + filtering)
        └── notifier.py        → Telegram Bot API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;github_search.py&lt;/code&gt; hits GitHub's Issues Search API with a query like &lt;code&gt;is:open is:issue language:Python label:"good first issue"&lt;/code&gt;, extracts the fields that matter (title, repo, body, labels, comment count), and returns a list.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scorer.py&lt;/code&gt; sends each issue plus a short profile of my stack to Gemini and asks for a JSON response: a score from 1-10 and a one-line reason. Anything below 6 gets dropped. The top 5 by score go to Telegram.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;notifier.py&lt;/code&gt; formats the results as an HTML message and POSTs to the Telegram Bot API. I used HTML over Telegram's Markdown format because repo names often contain underscores that break Markdown parsing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lambda_function.py&lt;/code&gt; is thin on purpose. It just calls the three functions in order and returns a status dict.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw_issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_issues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;extract_issue_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw_issues&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;top_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_and_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;send_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuesFound&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuesSent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_matches&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Infrastructure is defined in a SAM template: one Lambda function, one EventBridge schedule, one IAM execution role scoped to CloudWatch Logs only (the function makes outbound HTTPS calls to GitHub, Gemini, and Telegram, it doesn't need any AWS service permissions beyond logging).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Lambda and not EC2
&lt;/h2&gt;

&lt;p&gt;This job runs for about 45 seconds, once a day. An EC2 instance would idle for 23 hours 59 minutes burning credits. Lambda charges only for actual execution time, and at this volume, the cost sits comfortably inside AWS's permanent free tier (1M requests/month, 400K GB-seconds/month). Not the trial credits, the always-free tier. The agent costs $0.00 to run.&lt;/p&gt;

&lt;p&gt;I also have a trial AWS account with a finite credit budget. SAA-C03 labs need those credits more than a daily cron job does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;Three things broke, in order of how annoying they were to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Gemini's free tier has a 20 requests/day cap on some accounts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I knew the published number was around 1,500 requests/day for Flash-Lite. What I didn't know is that the actual limit varies by account, project, and billing state. The real number for my account turned out to be 20. I found this out by running the scorer against 15 issues back-to-back and watching every single one fail with a 429.&lt;/p&gt;

&lt;p&gt;The error body told me everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"quotaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GenerateRequestsPerDayPerProjectPerModel-FreeTier"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"quotaValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd built retry logic assuming these were per-minute rate limits, the kind you can wait out. But 20 per day means once you've hit the ceiling, waiting 5 seconds and retrying just wastes another request. I ripped the retry block out, dropped &lt;code&gt;max_results&lt;/code&gt; from 15 to 8, and moved on. The lesson: check your actual quota from the API response body, not the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;python-dotenv&lt;/code&gt; crashed the Lambda function.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;load_dotenv()&lt;/code&gt; locally to read &lt;code&gt;.env&lt;/code&gt; files. Lambda doesn't use &lt;code&gt;.env&lt;/code&gt; files, it reads environment variables from its own configuration. I'd deliberately excluded &lt;code&gt;python-dotenv&lt;/code&gt; from the Lambda package to keep dependencies light. What I forgot to do was make the import conditional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'dotenv'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix was two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
    &lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. GitHub's API timed out twice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Read timeout on the search call. Not a rate limit, not an auth issue, just a slow moment on GitHub's end. The fix was bumping the timeout from 10 seconds to 20 and adding a retry with backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The first clean run
&lt;/h2&gt;

&lt;p&gt;After the Gemini quota reset overnight, the agent ran on its EventBridge schedule for the first time without me touching anything.&lt;/p&gt;

&lt;p&gt;CloudWatch logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;08:00:25  INIT_START
08:00:26  Starting daily OSS contribution scan...
08:00:26  GitHub API rate limit remaining: 9
08:00:26  Found 8 candidate issues from GitHub
08:01:10  4 issues passed the scoring bar
08:01:10  Digest sent.
08:01:10  Duration: 44490ms  Billed: 44884ms  Memory: 63MB/256MB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;45 seconds. 63MB peak memory. $0.00.&lt;/p&gt;

&lt;p&gt;What landed in Telegram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;8/10&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="na"&gt;Alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add email (SMTP), Slack and a generic webhook&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SikamikanikoBG/homelab-monitor&lt;/span&gt;
&lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Backend focus on Python, API, and common integrations. Good fit.&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;7/10&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="na"&gt;Improvements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Integration tests should be async&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;langchain-ai/langchain-google&lt;/span&gt;
&lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Backend focus, Python, and testing align well. Scope is clear.&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;7/10&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="s"&gt;API keys shows two keys when only one is added&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RunestoneInteractive/rs&lt;/span&gt;
&lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Backend bug in Python/Django with security implications.&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;7/10&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="s"&gt;Add a Canada PIPEDA policy profile as a data-driven config&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maziyarpanahi/openmed&lt;/span&gt;
&lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Good fit for backend, Python, and security interest.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Django API key bug is the one I'll probably look at first. It's a backend bug with security implications, which is my exact lane, and the scope is clear enough to get into in an afternoon.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scoring prompt
&lt;/h2&gt;

&lt;p&gt;The most important thing in the whole system is &lt;code&gt;PROFILE_CONTEXT&lt;/code&gt;, the block of text Gemini reads before scoring each issue. If this is wrong, the scores are useless.&lt;/p&gt;

&lt;p&gt;Mine looks roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend developer, ~5 years experience.
Core stack: Python, Django, DRF, FastAPI, PostgreSQL, Redis, Docker.
Interested in: security-related issues (OWASP-adjacent), backend bugs,
API design issues, Django/FastAPI specifically.
NOT looking for: frontend-only issues, niche ML/data-science,
documentation-only unless very quick wins.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also tell Gemini to score down issues with 10+ comments (likely already claimed) and issues with empty bodies (hard to act on without knowing what's actually wanted).&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;It's a SAM template, so deployment is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam build
sam deploy &lt;span class="nt"&gt;--guided&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run asks for the stack name, region, and the three secrets (Gemini key, Telegram bot token, chat ID). Subsequent runs are just &lt;code&gt;sam build &amp;amp;&amp;amp; sam deploy&lt;/code&gt;. SAM creates the Lambda function, IAM role, and EventBridge rule as a single CloudFormation stack.&lt;/p&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/Jpeg-create/OSS-Contrib-Scout" rel="noopener noreferrer"&gt;github.com/Jpeg-create/OSS-Contrib-Scout&lt;/a&gt; if you want to run your own version. The &lt;code&gt;PROFILE_CONTEXT&lt;/code&gt; in &lt;code&gt;scorer.py&lt;/code&gt; is the main thing you'd change.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Two more agents are in progress using the same architecture: OSS-Solution-Drafter (on-demand fix drafting via DeepSeek when I decide to pursue an issue) and Job-Scout (daily job listing digest with tailored cover letter drafts). Both will get their own posts once they're running.&lt;/p&gt;




&lt;p&gt;The code is straightforward. The interesting parts were the quota discovery and working out which retry logic actually helps versus which makes things worse. Building it taught me more about Lambda, EventBridge, and SAM than any course section covering the same topics, which was the point.&lt;/p&gt;

</description>
      <category>python</category>
      <category>aws</category>
      <category>serverless</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a Distributed Rate Limiter for FastAPI with Redis (Sliding Window Algorithm)</title>
      <dc:creator>R.A. Olanrewaju</dc:creator>
      <pubDate>Sun, 08 Mar 2026 16:12:38 +0000</pubDate>
      <link>https://dev.to/jpegcreate/building-a-distributed-rate-limiter-for-fastapi-with-redis-sliding-window-algorithm-5h10</link>
      <guid>https://dev.to/jpegcreate/building-a-distributed-rate-limiter-for-fastapi-with-redis-sliding-window-algorithm-5h10</guid>
      <description>&lt;h2&gt;
  
  
  Building a Distributed Rate Limiter for FastAPI with Redis
&lt;/h2&gt;

&lt;p&gt;Every API eventually runs into the same problem.&lt;/p&gt;

&lt;p&gt;A bot, scraper, or even a buggy client suddenly starts sending thousands of requests per second. When that happens, your server slows down, your database struggles, and real users start seeing errors.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of situation &lt;strong&gt;rate limiting&lt;/strong&gt; is meant to prevent.&lt;/p&gt;

&lt;p&gt;Recently I built &lt;strong&gt;RateGuard&lt;/strong&gt;, a small Python library that adds distributed rate limiting to FastAPI using Redis. In this post I want to walk through how it works and the design decisions behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built RateGuard
&lt;/h2&gt;

&lt;p&gt;While working with FastAPI, I looked at a few rate limiting libraries. Most of them had at least one issue.&lt;/p&gt;

&lt;p&gt;Some only support &lt;strong&gt;in-memory limits&lt;/strong&gt;, which means they break once your API runs on multiple servers.&lt;/p&gt;

&lt;p&gt;Others work in distributed setups but require more infrastructure than I wanted.&lt;/p&gt;

&lt;p&gt;So I decided to build something simple with a few goals in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;easy to plug into FastAPI&lt;/li&gt;
&lt;li&gt;works across multiple servers&lt;/li&gt;
&lt;li&gt;accurate under heavy traffic&lt;/li&gt;
&lt;li&gt;simple enough to understand and maintain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That became &lt;strong&gt;RateGuard&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Rate Limiting?
&lt;/h2&gt;

&lt;p&gt;Rate limiting controls how many requests a user can send to an API within a certain time period.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a user can send &lt;strong&gt;10 requests per minute&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;after the limit is reached they receive a &lt;code&gt;429 Too Many Requests&lt;/code&gt; response&lt;/li&gt;
&lt;li&gt;after the time window passes the limit resets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple analogy is a coffee shop rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One free coffee per customer per hour.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The barista keeps track of who got a coffee and when. If you come back too soon, you have to wait.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use a Counter?
&lt;/h2&gt;

&lt;p&gt;A very common approach is to use a simple counter that resets every minute.&lt;/p&gt;

&lt;p&gt;The problem is that this method can be abused.&lt;/p&gt;

&lt;p&gt;Imagine your limit is &lt;strong&gt;10 requests per minute&lt;/strong&gt; and the counter resets at exactly &lt;strong&gt;12:00:00&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A user could do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send 10 requests at 11:59:55&lt;/li&gt;
&lt;li&gt;the counter resets at 12:00:00&lt;/li&gt;
&lt;li&gt;send another 10 requests at 12:00:05&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That ends up being &lt;strong&gt;20 requests in about 10 seconds&lt;/strong&gt;, even though the limit is supposed to be 10 per minute.&lt;/p&gt;

&lt;p&gt;This issue is called the &lt;strong&gt;fixed window problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sliding Window Approach
&lt;/h2&gt;

&lt;p&gt;To avoid this problem, RateGuard uses the &lt;strong&gt;sliding window algorithm&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of resetting at fixed times, it always looks back a certain number of seconds from the current request.&lt;/p&gt;

&lt;p&gt;The logic looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a request arrives at time &lt;code&gt;T&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;check all requests between &lt;code&gt;T - window&lt;/code&gt; and &lt;code&gt;T&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;remove anything older than the window&lt;/li&gt;
&lt;li&gt;count the remaining requests&lt;/li&gt;
&lt;li&gt;if the count is below the limit, allow the request&lt;/li&gt;
&lt;li&gt;otherwise return a &lt;code&gt;429&lt;/code&gt; response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Going back to the coffee shop example, instead of resetting every hour on the clock, the barista asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did this person get a coffee in the last 60 minutes?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The time window moves forward with every request.&lt;/p&gt;




&lt;h2&gt;
  
  
  Basic Architecture
&lt;/h2&gt;

&lt;p&gt;RateGuard sits between incoming requests and your FastAPI application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client Request
      |
      v
FastAPI Server
      |
      v
RateGuard Middleware
      |
      v
Redis Sorted Set
      |
      v
Allow or Block Request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis stores request timestamps so every server in the system can see them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Redis?
&lt;/h2&gt;

&lt;p&gt;Redis is a good fit for rate limiting for two main reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;p&gt;Rate limiting runs on &lt;strong&gt;every request&lt;/strong&gt;, so it has to be fast. Redis is an in-memory data store and can handle a huge number of operations per second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared state
&lt;/h3&gt;

&lt;p&gt;If your API runs on several servers, each one needs to know how many requests have already been made. Redis works as a shared store that all servers can read from and write to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using Redis Sorted Sets
&lt;/h2&gt;

&lt;p&gt;RateGuard stores request data inside a &lt;strong&gt;Redis Sorted Set&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A sorted set stores values with a score. The score determines the order.&lt;/p&gt;

&lt;p&gt;In this case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;score&lt;/strong&gt; is the request timestamp&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;value&lt;/strong&gt; is a unique request ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key: ratelimit:192.168.1.1

1709856060000 -&amp;gt; req_abc123
1709856080000 -&amp;gt; req_def456
1709856100000 -&amp;gt; req_xyz789
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each request, RateGuard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;removes entries older than the time window&lt;/li&gt;
&lt;li&gt;counts the remaining entries&lt;/li&gt;
&lt;li&gt;decides whether to allow or block the request&lt;/li&gt;
&lt;li&gt;records the new request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach works well even when multiple servers are handling traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing RateGuard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;rate-guardian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will also need a Redis instance. I used Upstash Redis, which has a generous free tier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rateguard&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RateGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RateLimitMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_limit&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RateGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;redis_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPSTASH_REDIS_REST_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;redis_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPSTASH_REDIS_REST_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;RateLimitMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API is protected by RateGuard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding the middleware, every endpoint is automatically protected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Per Route Limits
&lt;/h2&gt;

&lt;p&gt;Sometimes you want stricter limits for certain endpoints.&lt;/p&gt;

&lt;p&gt;For example, a search endpoint that queries a database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@rate_limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;/search&lt;/code&gt; only allows &lt;strong&gt;5 requests per minute&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Response Headers
&lt;/h2&gt;

&lt;p&gt;RateGuard includes useful headers in responses.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X-RateLimit-Limit&lt;/td&gt;
&lt;td&gt;Maximum allowed requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X-RateLimit-Remaining&lt;/td&gt;
&lt;td&gt;Requests left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X-RateLimit-Reset&lt;/td&gt;
&lt;td&gt;Seconds until reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry-After&lt;/td&gt;
&lt;td&gt;Only present on 429 responses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These help clients know when to slow down.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens If Redis Fails?
&lt;/h2&gt;

&lt;p&gt;One design decision I made was to &lt;strong&gt;fail open&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Redis is temporarily unavailable, requests are allowed instead of blocked.&lt;/p&gt;

&lt;p&gt;For most APIs, blocking all traffic because Redis is down would be worse than briefly running without rate limiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Logic Example
&lt;/h2&gt;

&lt;p&gt;Here is a simplified version of the main logic:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;oldest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zremrangebyscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zcard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a pipeline groups the Redis operations together and avoids race conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few improvements I plan to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;support for standard Redis deployments&lt;/li&gt;
&lt;li&gt;rate limiting by user ID&lt;/li&gt;
&lt;li&gt;an optional token bucket algorithm&lt;/li&gt;
&lt;li&gt;better metrics and monitoring&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;rate-guardian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/Jpeg-create/rate-guard" rel="noopener noreferrer"&gt;https://github.com/Jpeg-create/rate-guard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/rate-guardian/" rel="noopener noreferrer"&gt;https://pypi.org/project/rate-guardian/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find it useful, a ⭐ on GitHub means a lot.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>redis</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
