<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Aryan Khurana on Medium]]></title>
        <description><![CDATA[Stories by Aryan Khurana on Medium]]></description>
        <link>https://medium.com/@aryank1511?source=rss-edbdeda7e4a3------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*E_ApcfPqpKHNgyhHHYvv7Q.jpeg</url>
            <title>Stories by Aryan Khurana on Medium</title>
            <link>https://medium.com/@aryank1511?source=rss-edbdeda7e4a3------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 09 Jun 2026 08:31:12 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@aryank1511/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Our production database was failing under load. Here’s the one-line fix.]]></title>
            <link>https://medium.com/beyond-localhost/our-production-database-was-failing-under-load-heres-the-one-line-fix-05cb1f505870?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/05cb1f505870</guid>
            <category><![CDATA[saas]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[pgbouncer]]></category>
            <category><![CDATA[supabase]]></category>
            <category><![CDATA[postgresql]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Thu, 14 May 2026 17:51:10 GMT</pubDate>
            <atom:updated>2026-05-14T17:51:10.795Z</atom:updated>
            <content:encoded><![CDATA[<p><em>psycopg3 auto-prepares statements by default. Under PgBouncer transaction pooling, that quietly destroys you.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dEW9RMWghJI2CghCBqEYtg.jpeg" /></figure><p>Production was broken for our app <a href="https://rezzy.dev">Rezzy</a>. Database calls were failing under load, and we had no idea why.</p><p>The errors weren’t constant. Light traffic was fine. But once requests stacked up, things started falling apart at the database layer. Queries that had run thousands of times without issue were suddenly throwing errors. We kept looking at our code. Nothing had changed.</p><p>That’s the worst kind of bug. The kind where you’re staring at code that looks correct, in an environment that worked yesterday, and the logs are pointing at something you don’t fully understand yet.</p><h3>What We’re Working With</h3><p>Rezzy runs on Google Cloud Run. If you haven’t used it, Cloud Run is Google’s managed container platform. It auto-scales based on traffic, which is great for a product with spiky load. More requests come in, more instances spin up. Things quiet down, instances scale back.</p><p>The database side of that story gets complicated fast. Every Cloud Run instance maintains its own pool of connections to Postgres. As instances multiply under load, so do connections. Postgres isn’t designed to handle that at scale. Each connection is a process on the server. Memory, file descriptors, overhead. The database starts struggling well before you run out of application capacity.</p><p>So we connect through Supabase’s transaction pooler URL, which runs PgBouncer under the hood. PgBouncer sits between your app and Postgres, maintaining a small fixed set of real connections and handing them out as needed. We run it in transaction pooling mode, which means a client only holds a real Postgres connection for the duration of a single transaction. The moment that transaction commits, the connection goes back in the pool for someone else to use.</p><p>This is an extremely efficient setup. Ten thousand clients can share twenty Postgres connections. For Cloud Run, it’s the right call.</p><p>It’s also what made the bug so hard to find.</p><p>Our ORM is SQLModel, which sits on top of SQLAlchemy and uses psycopg3 as the actual Postgres driver. That chain matters. Each layer has its own behavior, and one of them was working against us without us knowing.</p><h3>How a Query Actually Travels to Postgres</h3><p>Before getting into what broke, it helps to understand the full journey a query takes. Most engineers have a rough mental model of this, but the details matter here.</p><p>When your code calls something like session.execute(query), here&#39;s the real sequence:</p><p>Your ORM hands the SQL string down to psycopg3. psycopg3 either opens a network connection to the database or grabs an existing one from its pool. It then sends the query over that connection using Postgres’s wire protocol. Postgres receives the bytes, parses the SQL into a syntax tree, figures out an execution plan (which indexes to scan, how to order joins, etc.), and runs it. Results come back over the wire and get decoded into Python objects.</p><p>That parse and plan step happens every single time. For a query that runs hundreds of times per second, Postgres is doing the same work over and over on the same SQL string.</p><p>Prepared statements exist to short-circuit that. You send Postgres a query once, it parses and plans it, caches the result under a name, and from then on you just send the name plus parameter values. Postgres skips straight to execution.</p><p>In raw SQL it looks like this:</p><pre>PREPARE my_query (int) AS SELECT CAST($1 AS INTEGER);</pre><pre>EXECUTE my_query(1);<br>EXECUTE my_query(2);<br>EXECUTE my_query(3);</pre><pre>DEALLOCATE my_query;</pre><p>Good optimization. Less repeated work. Faster queries at volume.</p><p>Here’s the thing: psycopg3 does this automatically. You don’t write any of that PREPARE code yourself. The driver watches how many times a query runs on a connection, and after 5 executions it prepares the statement silently, in the background. The next execution and every one after that uses the cached plan.</p><p>That threshold is controlled by a setting called prepare_threshold. The default is 5. Most people never touch it. We hadn&#39;t.</p><h3>Why This Explodes With Transaction Pooling</h3><p>Prepared statements are connection-local. When psycopg3 sends PREPARE _pg3_0 AS ... on connection A, that statement lives only on connection A, in the memory of that specific Postgres backend process. Connection B has never heard of it.</p><p>In session pooling mode, this doesn’t matter. Your client holds the same Postgres connection for its entire session, so prepared statements accumulate and stay available.</p><p>In transaction pooling mode, your connection changes after every transaction. PgBouncer is actively shuffling real Postgres connections between clients to maximize utilization. You might get connection A for one transaction and connection C for the next.</p><p>Here’s the exact sequence that was killing us:</p><p>Transaction 1 borrows Postgres connection A from the pool. psycopg3 runs the query. It’s the 5th execution, so the driver automatically prepares it: PREPARE _pg3_0 AS .... Transaction commits. Connection A goes back into the pool.</p><p>Some other client picks up connection A. Does their thing. Returns it.</p><p>Transaction 2 starts. It borrows connection B. psycopg3 still has _pg3_0 in its local cache and sends EXECUTE _pg3_0 to connection B. Connection B has never seen _pg3_0. Postgres throws an error.</p><p>There’s a second failure mode that’s even more confusing. Two different clients can race to prepare the same statement name on the same connection:</p><p>Transaction 1 from client 1 borrows connection A. psycopg3 prepares _pg3_0. Returns connection A to the pool. Transaction 1 from client 2 then picks up connection A. Its psycopg3 instance also decides it&#39;s time to prepare its first query, also naming it _pg3_0. Postgres responds: &quot;a prepared statement called _pg3_0 already exists.&quot; That&#39;s the DuplicatePreparedStatementError.</p><p>Both failure modes have the same root cause: the driver tracks prepared statement state on a connection it doesn’t actually own persistently. From psycopg3’s perspective, it has a connection and a cache. It has no visibility into PgBouncer shuffling that connection to a dozen other clients between transactions.</p><p>Under light traffic, you rarely hit this. The pool might consistently hand you the same connection by chance. Under real load, with concurrent requests and PgBouncer actively multiplexing, the collision rate climbs and the errors stack up fast. Which is exactly what we saw. Fine in development, fine under low traffic, broken in production under load.</p><h3>Verifying It</h3><p>Before touching anything in production, we wanted to confirm what was actually happening. We wrote a small script that runs the same query 10 times on one connection, then queries pg_prepared_statements directly to see if psycopg3 had prepared anything:</p><pre>import asyncio<br>from sqlalchemy import text<br>from src.database.postgres.postgres_client import PostgresClient<br><br>async def main() -&gt; None:<br>    client = PostgresClient()<br>    try:<br>        async with client.engine.connect() as conn:<br>            for _ in range(10):<br>                await conn.execute(<br>                    text(&quot;SELECT CAST(:value AS INTEGER)&quot;),<br>                    {&quot;value&quot;: 1},<br>                )<br>            result = await conn.execute(<br>                text(<br>                    &quot;&quot;&quot;<br>                    SELECT name, statement<br>                    FROM pg_prepared_statements<br>                    WHERE name LIKE &#39;_pg3_%&#39;<br>                    ORDER BY name<br>                    &quot;&quot;&quot;<br>                )<br>            )<br>            rows = result.fetchall()<br>        print(f&quot;Prepared statements found: {len(rows)}&quot;)<br>        for name, statement in rows:<br>            print(f&quot;{name}: {statement}&quot;)<br>        assert not rows, &quot;psycopg prepared statements are still being created&quot;<br>    finally:<br>        await client.engine.dispose()<br>if __name__ == &quot;__main__&quot;:<br>    asyncio.run(main())</pre><p>Running this before the fix: prepared statements showing up in the results, _pg3_0 and friends, exactly as expected. The driver was preparing statements we never asked it to prepare.</p><h3>The Fix</h3><p>One line in the engine configuration:</p><pre>return create_async_engine(<br>    self.database_url,<br>    connect_args={<br>        &quot;prepare_threshold&quot;: None<br>    },<br>)</pre><p>Setting prepare_threshold to None tells psycopg3 to never automatically prepare any statement. Every query goes out as full SQL every time. The driver stays stateless across connection boundaries, which is exactly what you need when those boundaries are being managed by a pooler.</p><p>Run the verification script again after the fix: zero prepared statements. The assert passes.</p><p>We deployed it. The errors stopped.</p><h3>What You’re Giving Up</h3><p>Disabling prepared statements isn’t free. You lose the parse and plan cache, so Postgres does that work on every query. For most web application queries, this overhead is small relative to actual execution time and network latency. Postgres’s planner is fast. The tradeoff is worth it.</p><p>There is an alternative path. PgBouncer 1.21 added experimental support for proxying prepared statements in transaction mode, so the pooler itself intercepts and manages them on behalf of clients. It works in some setups, but there are still edge cases around DEALLOCATE behavior with psycopg3 that can bite you. Disabling auto-preparation is the simpler and more reliable fix if you&#39;re on transaction pooling.</p><p>The other option is switching to session pooling mode, which gives each client a persistent Postgres connection and makes prepared statements work correctly. The cost is that session mode is far less efficient for connection multiplexing. Depending on your traffic patterns and connection budget, that may or may not be acceptable.</p><h3>Key Takeaways</h3><ul><li>psycopg3 prepares statements automatically after 5 executions by default. This is an optimization, but it assumes the driver has a persistent connection underneath it.</li><li>PgBouncer’s transaction pooling assigns a different Postgres connection per transaction. Prepared statements prepared on one connection don’t exist on the next one your driver gets handed.</li><li>Setting prepare_threshold=None in your psycopg3 connect args disables auto-preparation entirely and keeps the driver stateless, which is what you need under a transaction pooler.</li><li>This bug hides under low traffic and surfaces under real load. If your database errors appear only when things get busy and the queries themselves look correct, this is worth checking immediately.</li><li>Querying pg_prepared_statements directly is the fastest way to confirm whether the fix actually worked. Write the test before and after.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=05cb1f505870" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/our-production-database-was-failing-under-load-heres-the-one-line-fix-05cb1f505870">Our production database was failing under load. Here’s the one-line fix.</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Supabase Actually Signs Your JWTs, and Why It Matters]]></title>
            <link>https://medium.com/beyond-localhost/how-supabase-actually-signs-your-jwts-and-why-it-matters-ecf007798834?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/ecf007798834</guid>
            <category><![CDATA[authentication]]></category>
            <category><![CDATA[supabase]]></category>
            <category><![CDATA[jwt]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Wed, 13 May 2026 00:06:01 GMT</pubDate>
            <atom:updated>2026-05-13T00:06:01.483Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>A breakdown of HS256 vs ES256, JWKS, and what you are really trusting when you accept a JWT.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JzoIdGerJyxEYcf08QYSIg.jpeg" /></figure><p>If you are building with Supabase, you are using JWTs for auth. They show up in your cookies, your authorization headers, your RLS policies. But most people have a pretty fuzzy mental model of what is actually happening under the hood. In this blog we will walk through all of it.</p><h3>Signing is not encryption</h3><p>Before anything else, this distinction needs to be clear because it trips a lot of people up.</p><p>Encryption scrambles data so nobody can read it without a key. Signing leaves the data completely readable but attaches a tamper-proof seal. JWTs use signing, not encryption.</p><p>You can decode the payload of any JWT right now without any key at all:</p><pre>echo &quot;eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0&quot; | base64 --decode<br># {&quot;sub&quot;:&quot;1234567890&quot;,&quot;name&quot;:&quot;John Doe&quot;,&quot;admin&quot;:true,&quot;iat&quot;:1516239022}</pre><p>The payload is not a secret. What the signature protects is the integrity of that payload. It lets your server say “this was created by whoever holds the signing key, and nothing has been changed since.” That is what you are verifying on every authenticated request.</p><p>If you actually need confidential claims in a JWT, you want JWE (JSON Web Encryption). That is a different standard entirely, and Supabase does not use it by default.</p><h3>The symmetric approach: HS256</h3><p>HS256 stands for HMAC using SHA-256.</p><p>SHA-256 is a hash function. Feed it any input, you get back a fixed 256-bit fingerprint. Same input always produces the same output, and you cannot reverse it. HMAC takes SHA-256 and mixes in a secret key, so the signature is essentially HMAC-SHA256(secret, header + &quot;.&quot; + payload).</p><p>To verify a JWT, the server recomputes that value using its own copy of the secret and checks whether it matches the signature in the token. Match means the token is valid. No match means 401.</p><p>This is called symmetric because the same key does both jobs. Creating a signature requires the secret. Verifying one also requires the secret. Both sides have to hold the exact same key.</p><p>In a Supabase project, this means you copy the JWT secret from your dashboard and paste it into your environment. Now it lives in at least two places: Supabase’s auth server and your own infrastructure. Every service that needs to verify tokens needs a copy.</p><p>This works, and a lot of production apps run on it just fine. But the failure mode is worth thinking about. If that secret leaks:</p><ul><li>An attacker can forge JWTs for any user, including ones that do not exist</li><li>They can craft tokens with role: service_role and bypass all your row-level security</li><li>You have to rotate the secret immediately, which means updating every environment that has a copy and invalidating every active user session at the same time</li></ul><p>Symmetric is not inherently insecure. The issue is that the more places the secret lives, the more surface area you have to defend. And when it fails, it fails completely.</p><h3>The asymmetric approach: ES256</h3><p>ES256 uses elliptic curve cryptography instead of a shared secret. Rather than one key that both sides hold, you have two mathematically linked keys.</p><p>The private key lives on Supabase’s auth server. It never leaves. You cannot download it, and you never see it as a developer. It is used to create signatures.</p><p>The public key is meant to be shared. It can only verify signatures, not create them. The math linking the two keys makes this strictly one-directional: having the public key gives you no ability to produce a valid signature.</p><p>This is the meaningful shift from HS256. In the symmetric model, anything that can verify a token holds enough information to forge one. In the asymmetric model, verification and signing are decoupled. Your server can verify tokens without touching anything that could be used to mint a fake one.</p><p>Supabase uses ES256 specifically, which is elliptic curve DSA on the P-256 curve. Compared to RSA, elliptic curve gives you equivalent security with smaller keys and faster operations. That matters because JWT verification is happening on every single authenticated request.</p><p>One thing worth knowing: Supabase historically defaulted to HS256 and added asymmetric JWT support more recently. Which algorithm your project uses depends on when it was created and your project settings. Check your dashboard if you are not sure.</p><p>Since anyone doing verification needs the public key, Supabase exposes it at a standard endpoint called a JWKS (JSON Web Key Set):</p><pre>https://your-project.supabase.co/auth/v1/.well-known/jwks.json</pre><p>The response is a JSON object containing an array of public keys. Each entry looks something like this:</p><pre>{<br>  &quot;kty&quot;: &quot;EC&quot;,<br>  &quot;crv&quot;: &quot;P-256&quot;,<br>  &quot;x&quot;: &quot;...&quot;,<br>  &quot;y&quot;: &quot;...&quot;,<br>  &quot;kid&quot;: &quot;abc123&quot;,<br>  &quot;alg&quot;: &quot;ES256&quot;<br>}</pre><p>kty and crv tell you it is an elliptic curve key on the P-256 curve. x and y are the actual public key coordinates. kid is a short identifier for this specific key.</p><p>Every JWT Supabase issues includes a kid field in its header (the first segment, not the payload):</p><pre>{ &quot;alg&quot;: &quot;ES256&quot;, &quot;typ&quot;: &quot;JWT&quot;, &quot;kid&quot;: &quot;abc123&quot; }</pre><p>Verification works like this: read the kid from the header, find the matching key in the JWKS, verify the signature using that key. If the kid is not in your cache, refetch the JWKS endpoint. This is also how key rotation works without breaking existing tokens. If the key still is not there after a refetch, the token is invalid.</p><h3>Who actually does the verifying</h3><p>This is something that confuses people coming from HS256.</p><p>In the HS256 world, verification can happen on Supabase’s servers because they hold the shared secret. In the ES256 world, you verify tokens yourself using the public key. That is kind of the whole point of going asymmetric.</p><p>Your server fetches the JWKS on startup, caches the public keys in memory, and on every authenticated request it reads the kid from the incoming JWT, looks up the right key, and runs the ES256 verification locally. No round-trip to Supabase, just math. The only time it hits the JWKS endpoint again is when a key rotates and a kid shows up that is not in the cache yet.</p><p>The upside is that even if someone somehow got your public key, they gain nothing. The only thing that could let someone forge tokens is the private key, which never left Supabase’s auth server.</p><h3>Key takeaways</h3><ul><li>JWTs are signed, not encrypted. The payload is readable by anyone. Do not put secrets in it.</li><li>HS256 uses one shared secret for both signing and verification. Any party that can verify a token holds enough information to forge one, and a leaked secret means rotating everything immediately.</li><li>ES256 splits signing and verification into separate keys. Your application only ever holds the public key, which cannot be used to forge tokens.</li><li>JWKS is how public keys get distributed. Your server fetches and caches the JWKS, and verification happens locally on every request.</li><li>In the asymmetric model, you are doing the JWT verification yourself, not Supabase.</li><li>Check which algorithm your project is actually using. The default has changed over time, and older projects may still be on HS256.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ecf007798834" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/how-supabase-actually-signs-your-jwts-and-why-it-matters-ecf007798834">How Supabase Actually Signs Your JWTs, and Why It Matters</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What a Hackathon Workshop Taught Us About Async Database Architecture in FastAPI]]></title>
            <link>https://medium.com/beyond-localhost/what-a-hackathon-workshop-taught-us-about-async-database-architecture-in-fastapi-830180bbfb3b?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/830180bbfb3b</guid>
            <category><![CDATA[fastapi]]></category>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[ai-engineering]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Sat, 02 May 2026 11:07:06 GMT</pubDate>
            <atom:updated>2026-05-02T11:09:22.743Z</atom:updated>
            <content:encoded><![CDATA[<h4>How we diagnosed and fixed connection pool exhaustion in a production FastAPI + SQLAlchemy stack.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/300/1*kOz0KFDHu1uGtsWXYfHr0A.png" /></figure><p>First, a quick win: as of writing this, <a href="https://rezzy.dev">Rezzy</a> just hit 1,000 users. 🎉</p><p>Now let me tell you about the time we almost tanked the whole thing in front of a room full of developers.</p><p>We recently shipped a feature that’s essentially Cursor for resumes. You chat with an AI agent, ask it to rewrite sections, and it understands you. It pulls from your profile, remembers context across sessions, and writes changes back in real time.</p><p>This feature talks to our Postgres database a lot. Every message gets stored, profile data gets fetched, updates get written back. It worked perfectly in local testing and staging. We shipped it.</p><p>Then we sponsored a hackathon.</p><h3>The Incident</h3><p>We sponsored a hackathon and ran a hands-on workshop where we gave attendees free access to the product and walked them through it live. The whole point was to get a room full of developers actually using Rezzy in real time.</p><p>And that’s exactly what they did.</p><p>Mid-workshop, everything stopped working. Every button. Every API call. Pure timeouts across the board.</p><p>We were fairly confident it was connection pool exhaustion but we had no time to investigate properly mid-workshop. So we did the only thing you can do in that moment: redeployed the backend, restarted our Cloud Run service, and watched everything come back online.</p><p>Until it broke again three hours later.</p><p>We kept redeploying as a band-aid until we actually sat down and fixed the root cause. This post is that story.</p><h3>Connection Pools: A Quick Primer</h3><p>Before getting into what went wrong, let me make sure we’re on the same page about how connection pools work because the rest of this post depends on it.</p><p>Opening a raw TCP connection to Postgres is expensive. You don’t want to do it fresh on every API request. So SQLAlchemy maintains a pool of pre-opened connections that your app borrows and returns.</p><p>Two numbers control pool capacity:</p><ul><li><strong>pool_size</strong> is the number of connections kept open and idle, ready to use at any time.</li><li><strong>max_overflow</strong> is how many extra connections can be opened on top of that during a burst of traffic.</li></ul><p>Together they define the maximum number of simultaneous Postgres connections your app can have per instance. Once you hit that ceiling, new requests have to wait. If nothing returns a connection before pool_timeout fires, the request fails.</p><p>A connection is either:</p><ul><li><strong>Checked in</strong> meaning it’s idle in the pool and available.</li><li><strong>Checked out</strong> meaning it’s actively being used by a request.</li></ul><p>The pool starts empty and warms up lazily as requests come in. That’s the model. When it works well you barely think about it. When it breaks, everything breaks at once.</p><h3>What Was Actually Wrong</h3><p>The logs confirmed pool exhaustion. But the real question was why connections weren’t being returned. That came down to four compounding problems.</p><h3>Problem 1: Sync Database Access Inside an Async App</h3><p>Our backend is FastAPI, which is fully async. But the database layer wasn’t. We were using a synchronous SQLAlchemy engine with sync Session objects inside async request handlers.</p><pre># Sync engine in an async app<br>return create_engine(self.database_url)<br><br># Sync session dependency<br>def get_db() -&gt; Generator[Session, None, None]:<br>    yield from postgres_client.get_session()</pre><p>When a sync database call runs inside an async framework it blocks the entire event loop while it waits on I/O. No other coroutines can run. Requests pile up. Connections stay checked out longer than they should. Under concurrent load this compounds really fast.</p><h3>Problem 2: Blocking I/O in a High-Frequency Middleware Path</h3><p>We had rate-limiting middleware that ran on every single inbound API request. Inside that middleware there was a synchronous database lookup.</p><pre># Sync DB call inside async middleware<br>with Session(self._engine) as session:<br>    row = session.get(Subscription, uid)</pre><p>Middleware is a multiplier. Inefficiencies here hit every request, not just specific endpoints. A blocking DB call in middleware under concurrent traffic is a really reliable way to starve your event loop.</p><h3>Problem 3 (The biggest culprit): Long-Lived Sessions Across AI Workflows</h3><p>The AI agent feature was holding a single database session open for the entire duration of an AI workflow. That workflow includes model inference, tool calls, and streaming responses which can easily run 10 to 30 seconds end to end.</p><p>The thing is you don’t need a database connection for any of that. You need it for the roughly 50ms you’re actually reading or writing data. Holding a checked-out connection while waiting on a model response is pure waste and under concurrent usage that waste adds up quickly.</p><h3>Problem 4: Multiple Engines, Multiple Pools</h3><p>Different parts of the app were each instantiating their own SQLAlchemy engines independently.</p><p>This is a subtle but important mistake. Every SQLAlchemy engine owns its own connection pool. If your workers and your API handlers each have separate engines, you can blow past Postgres’s connection limit even when each individual pool looks healthy in isolation. You end up with more total connections than you intended and no single place to observe or control them.</p><h3>Reproducing It Locally</h3><p>Before fixing anything, I needed to be able to reproduce the exhaustion reliably in a local environment. There’s no point guessing at a fix if you can’t verify it actually works.</p><p>To simulate the load I used <a href="https://github.com/rakyll/hey">hey</a>, a straightforward HTTP load testing tool.</p><pre>brew install hey<br><br># Warm up, 20 concurrent requests each holding a connection for 20 seconds<br>hey -n 20 -c 20 -t 60 &quot;http://localhost:8000/your-slow-endpoint?seconds=20&quot;<br><br># Reproduce exhaustion, enough concurrent load to exceed pool_timeout<br>hey -n 30 -c 30 -t 90 &quot;http://localhost:8000/your-slow-endpoint?seconds=60&quot;</pre><p>The -n flag is total requests, -c is concurrent workers, and -t is client-side timeout. The goal with the second command is to have enough concurrent sessions holding connections long enough that the waiting requests hit pool_timeout. That&#39;s exactly the production failure mode we were seeing.</p><p>With a reliable way to reproduce the error on demand I could now validate fixes instead of shipping and hoping.</p><h3>The Fix</h3><p>The fix was not “increase the pool size.” That would have been a band-aid. The real problem was how the app was using the pool. Holding connections too long, blocking the event loop with sync I/O, and scattering pool ownership across the codebase. Here’s what we actually changed.</p><h3>Fix 1: Go Fully Async, End to End</h3><p>We replaced the sync engine with an async one using create_async_engine, switched to AsyncSession, and converted the FastAPI dependency from a sync generator to an async one.</p><pre># Before<br>def get_db() -&gt; Generator[Session, None, None]:<br>    yield from postgres_client.get_session()<br><br># After<br>async def get_db() -&gt; AsyncGenerator[AsyncSession, None]:<br>    async for session in postgres_client.get_session():<br>        yield session</pre><p>This sounds like a small change but it cascades through the entire codebase. Every repository method, service call, and controller now has to await its database operations. It&#39;s a lot of mechanical work but it&#39;s non-negotiable if you want your async framework to actually behave like one.</p><p>The payoff is that instead of blocking the event loop during database I/O, the app now yields control while the query is in flight. Other requests can make progress and connections come back to the pool faster.</p><h3>Fix 2: Fix the Middleware Hot Path</h3><pre># Before, sync DB call inside async middleware<br>with Session(self._engine) as session:<br>    row = session.get(Subscription, uid)<br><br># After, non-blocking<br>async with self._session_factory() as session:<br>    row = await session.get(Subscription, uid)</pre><p>This was the highest leverage fix per line of code. Because this middleware ran on every API request, unblocking it here had a bigger impact than fixing it anywhere else in the stack.</p><h3>Fix 3: Scope Sessions Tightly in AI Workflows</h3><p>For the AI agent we stopped injecting a long-lived session and instead passed in the session factory so the service opens a connection only when it actually needs one.</p><pre>@asynccontextmanager<br>async def _db_context(self) -&gt; AsyncGenerator[SomeService, None]:<br>    async with self.session_factory() as db:<br>        yield SomeService(db, SomeRepository(db))</pre><p>Now a connection is checked out for the duration of a database operation, not for the duration of a model call or streaming response. This is the right mental model for AI-adjacent workflows. Borrow a connection, do the DB work, return it immediately, then go do the slow AI stuff.</p><h3>Fix 4: One Engine, One Pool</h3><p>We consolidated all database access across API handlers, middleware, and background workers to use a single shared client instance and its session factory. No more independent engines scattered across the codebase.</p><p>One pool, one place to configure it, one place to observe it.</p><h3>Fix 5: Make Pool Behavior Explicit</h3><p>We moved pool configuration out of SQLAlchemy’s implicit defaults and into explicit environment-backed settings. Things like pool size, overflow capacity, timeout behavior, and whether connections should be health-checked before use are now deliberate decisions we can tune without touching code.</p><p>The specific values are something you should calibrate for your own workload and infrastructure. The point is that pool behavior should be something you consciously decide, not something a library quietly decides for you.</p><h3>Key Takeaways</h3><p>If you’re building async backends with AI workflows, here’s what to carry forward.</p><p><strong>Async all the way through or not at all.</strong> One blocking database call in a hot path negates the benefits of an async framework. This is easy to miss in FastAPI because it will happily accept sync dependencies and route handlers without complaining. It won’t tell you you’re doing it wrong.</p><p><strong>Middleware is a multiplier.</strong> Code running on every request has outsized impact on performance. Any blocking I/O there will surface under load before you feel it anywhere else in the stack.</p><p><strong>AI workflows and long-lived DB sessions don’t mix.</strong> If your code is waiting on a model response, a tool call, or a streaming output, it should not be holding a database connection. Open it for the data operation, close it immediately, then go do the slow AI work.</p><p><strong>One engine, one pool.</strong> If different parts of your app each create their own SQLAlchemy engine you have multiple pools with no unified view of connection usage. Centralize it.</p><p><strong>Make your pool observable and tunable.</strong> If you can’t see how many connections are checked in versus checked out at any given moment you’re flying blind. And if your pool settings are implicit library defaults you have no leverage when things go wrong in production.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=830180bbfb3b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/what-a-hackathon-workshop-taught-us-about-async-database-architecture-in-fastapi-830180bbfb3b">What a Hackathon Workshop Taught Us About Async Database Architecture in FastAPI</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting Up Production Logging in a FastAPI Microservice]]></title>
            <link>https://medium.com/beyond-localhost/setting-up-production-logging-in-a-fastapi-microservice-bd4201b0ec82?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/bd4201b0ec82</guid>
            <category><![CDATA[logging]]></category>
            <category><![CDATA[fastapi]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Sat, 18 Apr 2026 17:44:33 GMT</pubDate>
            <atom:updated>2026-04-18T17:45:05.458Z</atom:updated>
            <content:encoded><![CDATA[<h4>A practical walkthrough of why colorful console logs break in production, and how to replace them with structured, contextual logging using structlog.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KztqTGGr0ydj2d7DHI3udg.png" /></figure><h3>The Inspiration</h3><p>I run a company called <a href="https://www.rezzy.dev/"><strong>Rezzy</strong></a>, where we help engineers land interviews by building industry-standard resumes and cover letters using AI trained on real recruiters and hiring managers. Our stack is varied, but every backend microservice is written in Python with FastAPI.</p><p>When I first set everything up, I had a nice little logging setup going — the default uvicorn logger paired with colorama for pretty, colorful output. It looked great in the terminal, and it worked.</p><p>Thanks for reading! Subscribe for free to receive new posts and support my work.</p><p>That setup was fine in development. But we’ve grown a lot since then. Our user base is doubling every month, and more users means more bugs. Bug reports have been rolling in lately, and that is where my logging setup started to fall apart.</p><p>Our services run on GCP, so I’ve been using the log viewer that comes with it to dig through logs. The problem was that my logging was not built for that kind of environment. It worked for small scripts and dev APIs, but for production troubleshooting, it was nowhere near enough.</p><h3>Everything That Was Broken</h3><ul><li><strong>There was no structure.</strong> I had optimized for “pretty,” not parseable. Since nothing was emitted as JSON, the log parsers in third-party visualizers and search tools could not make sense of any of it. That alone was a dealbreaker.</li><li><strong>There was no request context.</strong> When multiple users hit the app at the same time, I had no way to trace a single request across all the logs it produced. Everything just smeared together.</li><li><strong>There was no user-level tracking.</strong> Since I never attached a user_id to log entries, I had no idea which logs belonged to which user. Debugging a specific bug report was basically guesswork.</li><li><strong>ANSI color codes were always on.</strong> My custom UvicornLikeFormatter unconditionally wrapped every field in Fore.* / Style.RESET_ALL. In production, logs end up in Cloud Run, CloudWatch, Datadog, or Loki looking like this: x1b[92m2026-…\x1b[0m | \x1b[94mINFO\x1b[0m | ..<br>That breaks parsers, breaks search, and looks awful in log UIs. Colors should only turn on when sys.stderr.isatty() is true, or when PYTHON_ENV != &quot;PROD&quot;.</li><li><strong>Every module imported the same root logger.</strong> That meant record.name was always root. I had lost the ability to filter or route by module. The convention is to call logging.getLogger(__name__) per module, or use a helper like get_logger(name).</li><li><strong>Third-party loggers were either silent or spammy.</strong> I set disable_existing_loggers: False and overrode uvicorn, which is fine, but I only configured the root logger and uvicorn itself. Everything else - SQLAlchemy, httpx, boto/botocore, OpenAI, Anthropic, LangChain, LangGraph, Stripe, Redis - was left to its defaults. A real production config pins the noisy ones (sqlalchemy.engine, botocore, httpx, urllib3, langchain, openai._base_client) to WARNING.</li></ul><h3>Building a Proper Logging Setup</h3><p>Alright, intro’s done. Let’s get into the actual tutorial on how to build production-grade logging for a FastAPI app.</p><h4>Step 1: The Logging Config File</h4><p>Create a single logging config file that gets shared across the entire application. Call it whatever you want — I use src/common/logger.py.</p><p>The first thing to know is that we’re using a library called <strong>structlog</strong>. It’s the backbone of this whole setup. It gives you structured logs (as dicts), a clean processor pipeline, and plays nicely with the standard library’s logging module.</p><h4>Step 2: Quiet the Noisy Libraries</h4><p>Before anything else, set per-library log levels. You don’t want to see every library’s internal chatter — it drowns out your own logs. If you let them all emit at INFO, your production output becomes 95% noise and 5% signal.</p><p>This map turns them down:</p><pre>_LIBRARY_LEVELS = {<br>    &quot;sqlalchemy.engine&quot;: &quot;WARNING&quot;,<br>    &quot;botocore&quot;: &quot;WARNING&quot;,<br>    &quot;httpx&quot;: &quot;WARNING&quot;,<br>    &quot;urllib3&quot;: &quot;WARNING&quot;,<br>    &quot;langchain&quot;: &quot;WARNING&quot;,<br>    &quot;openai._base_client&quot;: &quot;WARNING&quot;,<br>    # ...add others as needed<br>}</pre><p>Each library gets a level that matches how useful its output actually is.</p><h4>Step 3: The setup_logging Function</h4><p>This is called once, on app startup.</p><p>First thing we do is force stdout to flush after every newline instead of buffering in chunks:</p><pre>try:<br>    sys.stdout.reconfigure(line_buffering=True)<br>except AttributeError:<br>    # reconfigure doesn&#39;t exist on all stream types — fall back to default buffering<br>    pass</pre><p>This matters because when the app runs inside Docker, logs sit in memory until the buffer fills up, and then everything gets flushed at once. That is terrible for debugging — you’ll miss logs right up until the moment of a crash. The try/except exists because reconfigure isn’t available on every stream type, and if it fails we just accept the default buffering.</p><p>Next, pull the environment and log level from your settings:</p><pre>env = settings.PYTHON_ENV<br>level = settings.LOG_LEVEL</pre><h4>Step 4: The Shared Processors</h4><p>Now we get to the most important part — the <strong>shared processors</strong>.</p><p>A processor is a function that takes a log event (a dict) and returns a modified dict. They run in order, like a pipeline, for every log that flows through the system. Here is what each one does:</p><ul><li>merge_contextvars - Merges anything bound via structlog.contextvars.bind_contextvars(...). This is how per-request data (like user_id and request_id) gets attached automatically to every log inside that request. The middleware (coming up later) is what actually binds these values.</li><li>add_log_level - Pretty self-explanatory: attaches the log level (info, warning, error, etc.) to the event dict.</li><li>TimeStamper(fmt=&quot;iso&quot;, utc=True) - Adds a UTC timestamp. Always use UTC. If you log in local time, cross-service debugging becomes a nightmare the moment your services run in different regions.</li><li>StackInfoRenderer - If you pass stack_info=True to a log call, this renders the stack trace. Occasionally useful when you want context without an exception.</li><li>format_exc_info - If a log call includes exception info (e.g., logger.exception(...) or exc_info=True), this turns the traceback into a readable string. Without it, exceptions just… don’t show up in your logs. In production you might prefer dict_tracebacks for structured tracebacks, but format_exc_info works fine for both pretty and JSON output.</li><li>CallsiteParameterAdder - Attaches the source location (module, function, line number) to every log. So when you see an entry, you know exactly where in the code it came from. This is the equivalent of the %(module)s:%(funcName)s:%(lineno)d you had in the old stdlib formatter.</li></ul><p>Then we pick the renderer based on environment:</p><pre>if env == &quot;PROD&quot;:<br>    renderer = structlog.processors.JSONRenderer()<br>else:<br>    renderer = structlog.dev.ConsoleRenderer(colors=True)</pre><p>Production gets JSON so parsers can index it properly. Development gets colorful, human-readable output.</p><h4>Step 5: Configure structlog</h4><pre>structlog.configure(<br>    processors=shared_processors + [<br>        structlog.stdlib.ProcessorFormatter.wrap_for_formatter,<br>    ],<br>    wrapper_class=structlog.make_filtering_bound_logger(level),<br>    logger_factory=structlog.stdlib.LoggerFactory(),<br>    cache_logger_on_first_use=True,<br>)</pre><p>A quick walk-through of what each argument does:</p><ul><li>We hand structlog all the shared processors, plus wrap_for_formatter — which is the glue that lets structlog and the stdlib logging module collaborate.</li><li>wrapper_class=make_filtering_bound_logger(level) means the logger filters out events below the configured level <em>before</em> running any processors. Faster, and keeps your pipeline clean.</li><li>logger_factory=LoggerFactory() makes structlog loggers use stdlib loggers underneath. This is the key to unifying everything: stdlib logs (from uvicorn, SQLAlchemy, etc.) and structlog logs (from your code) both flow through the same pipeline.</li><li>cache_logger_on_first_use=True is a small performance win. Once get_logger(&quot;foo&quot;) is called, the configured logger is cached.</li></ul><h3>Step 6: The stdlib Side of the Pipeline</h3><p>Now we wire up the standard library side:</p><pre>handler = logging.StreamHandler(sys.stdout)<br>handler.setFormatter(<br>    structlog.stdlib.ProcessorFormatter(<br>        processor=renderer,<br>        foreign_pre_chain=shared_processors,<br>    )<br>)</pre><p>Two key pieces here:</p><ul><li>StreamHandler(sys.stdout) - Writes to stdout, not stderr. Stdout is the right default for container environments.</li><li>ProcessorFormatter - A special formatter that bridges stdlib logging into structlog’s processor system.</li></ul><p>Inside the formatter:</p><ul><li>processor=renderer is the final renderer (JSON in prod, Console in dev).</li><li>foreign_pre_chain=shared_processors is the important bit. Log events that originate from stdlib loggers - uvicorn, SQLAlchemy, Stripe - are “foreign” because they didn’t come through structlog. This argument runs them through the same processor pipeline before rendering.</li></ul><p>The result: whether a log comes from structlog.get_logger(__name__).info(...) in your code, <em>or</em> from uvicorn’s stdlib logger, it ends up formatted identically. One unified output format. This unification is the whole reason the structlog setup is slightly more involved - but it’s worth it.</p><h3>Step 7: Wire Up the Root Logger</h3><pre>root = logging.getLogger()<br>root.handlers.clear()<br>root.addHandler(handler)<br>root.setLevel(level)</pre><p>What each line does:</p><ul><li>logging.getLogger() with no args returns the root logger - the parent of every other stdlib logger.</li><li>root.handlers.clear() removes any handlers added elsewhere (by uvicorn’s default setup, basicConfig, whatever). This prevents duplicate log lines, which is critical because uvicorn adds its own handlers if you let it.</li><li>root.addHandler(handler) routes all logs through our unified handler.</li><li>root.setLevel(level) filters at the root at our configured level.</li></ul><p>By default, child loggers in Python propagate events up to the root. So once the root is configured, every library’s logger automatically uses our handler. That’s why we don’t have to configure each library individually — they inherit.</p><h3>Step 8: Per-Library Level Overrides</h3><pre>for name, lib_level in _LIBRARY_LEVELS.items():<br>    lib_logger = logging.getLogger(name)<br>    lib_logger.setLevel(lib_level)<br>    lib_logger.propagate = True</pre><p>For each entry in the map: grab the logger, set its level (so it filters its own noise <em>at the source</em>, before it ever reaches the root), and make sure propagate = True so events that pass the filter still bubble up.</p><p>Why set both? Two reasons:</p><ul><li>setLevel(WARNING) means the library won’t even <em>emit</em> INFO/DEBUG events. They’re discarded at the source, which is more efficient.</li><li>propagate = True (the default, but set explicitly here for safety) means whatever does pass the filter flows up to the root handler for formatting.</li></ul><p>The explicit propagate = True is slightly defensive. Some libraries set propagate = False on their own loggers to avoid double-logging, which would quietly break our setup. Forcing it to True guarantees every library’s output goes through our handler.</p><h3>Step 9: The Factory Function</h3><pre>def get_logger(name: str) -&gt; structlog.stdlib.BoundLogger:<br>    return structlog.get_logger(name)</pre><p>A thin wrapper so your app code imports get_logger from this module instead of calling structlog.get_logger directly. A few reasons this is worth it:</p><ul><li><strong>Consistent entry point.</strong> One place to change logging behavior app-wide.</li><li><strong>Typed return value.</strong> IDEs and type checkers know exactly what you’re getting back.</li><li><strong>Less coupling to structlog.</strong> If you ever want to swap the underlying library, you change one file.</li></ul><p>Usage is pretty simple:</p><pre>from src.common.logger import get_logger</pre><pre>logger = get_logger(__name__)</pre><pre>logger.info(&quot;order_created&quot;, order_id=123, total=49.99)</pre><h3>Wrapping Up</h3><p>That’s the setup. The short version of what changed:</p><ul><li>Logs are now structured JSON in production, readable color output in development.</li><li>Every log carries request context and user identity automatically.</li><li>Noisy third-party libraries are pinned to WARNING, so they stop burying real signal.</li><li>stdlib loggers and structlog loggers emit through a single unified pipeline.</li><li>Tracebacks, timestamps, and call sites are attached to every entry.</li></ul><p>It’s more code than my original pretty-logger setup, but the payoff is huge. Bug reports that used to take me an hour to track down now take a few minutes, because I can filter Cloud Logging by user_id, pull every log from the problem request, and actually see what happened.</p><p>If you’re running a FastAPI app in production and still relying on uvicorn defaults, I’d strongly encourage you to make the switch.</p><p>Catch you in the next one.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bd4201b0ec82" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/setting-up-production-logging-in-a-fastapi-microservice-bd4201b0ec82">Setting Up Production Logging in a FastAPI Microservice</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diary of an MLH SWE Fellow — Week 12 (Final Week) + My Experience]]></title>
            <link>https://medium.com/@aryank1511/diary-of-an-mlh-swe-fellow-week-12-final-week-my-experience-0a9852fa0706?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/0a9852fa0706</guid>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[apache-airflow]]></category>
            <category><![CDATA[mlh-fellowship]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Sat, 09 Aug 2025 18:37:54 GMT</pubDate>
            <atom:updated>2025-08-09T18:37:54.060Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diary of an MLH SWE Fellow — Week 12 (Final Week) + My Experience</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/686/1*YmtgJNKlAL8jUYLapW_Drg.jpeg" /></figure><p>This blog marks the final entry in a series I’ve been posting over the past three months. It began with my desire to document my experiences as an SWE Fellow at the MLH Fellowship, and in this blog, I’ll be reflecting on my last week and my overall journey as a fellow.</p><p>In my previous blog, I mentioned a PR I had submitted that was under review. That PR was eventually approved, merged, and the issue was closed. We also had our end-of-program demo with RBC, where we showcased everything we had accomplished since the mid-program demo and it went really well. Our final pod meeting was a chance for everyone to share their experiences.</p><p>Beforehand, each of us was sent a question via email to answer during the meeting. My question was: <em>“You have a unique experience in both SRE and SWE. Where does your heart lie moving forward? How do these skill sets complement each other?”</em> This was because I currently work full-time as an SRE at RBC. I shared my experiences, learned from others, and had a lot of fun during the discussion.</p><p>They congratulated us on graduating, and honestly, these past three months flew by so quickly that I didn’t even realize it.</p><p>Here’s everything I worked on so far:</p><ul><li><strong>Issues:</strong> <a href="https://github.com/apache/airflow/issues?q=is%3Aissue%20state%3Aclosed%20assignee%3AAryanK1511">https://github.com/apache/airflow/issues?q=is%3Aissue%20state%3Aclosed%20assignee%3AAryanK1511</a></li><li><strong>Pull Requests:</strong> <a href="https://github.com/apache/airflow/pulls?q=is%3Apr+is%3Aclosed+author%3AAryanK1511">https://github.com/apache/airflow/pulls?q=is%3Apr+is%3Aclosed+author%3AAryanK1511</a></li></ul><p>Along the way, I made new friends, gained valuable open-source experience (especially on such a large project), and fulfilled what was once a dream, joining and completing the MLH Fellowship.</p><p>This journey has been nothing short of amazing, and I hope you’ve enjoyed following this blog series as much as I’ve enjoyed writing it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0a9852fa0706" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diary of an MLH SWE Fellow — Week 10 and 11]]></title>
            <link>https://medium.com/@aryank1511/diary-of-an-mlh-swe-fellow-week-10-and-11-6c57e0feb7e6?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/6c57e0feb7e6</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[mlh-fellowship]]></category>
            <category><![CDATA[apache-airflow]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Mon, 04 Aug 2025 12:08:08 GMT</pubDate>
            <atom:updated>2025-08-04T12:08:08.698Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diary of an MLH SWE Fellow — Week 10 and 11</h3><h4>Prepping for the Final Week</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v1pmHtf9UhWY96TvC0cGFA.png" /></figure><h3>Introduction</h3><p>This blog builds on my previous post, where I shared that I was tackling my most complex issue yet. Although it started as a UI-related task, it required several backend prerequisites that hadn’t been implemented. As a result, I ended up opening five additional pull requests to introduce the necessary API changes.</p><p>Before I dive into the technical details, I want to talk about our recent pod merge standup. All our pod members joined, and we played Skribbl together, it was a blast! We also have our end-of-program demo coming up next week (Week 12).</p><p>I decided to combine my updates for Weeks 10 and 11 into one post, since I spent most of both weeks working on the same issue. I’m now basically done with the fellowship, there might be one more PR, but my main work is complete.</p><h3>Adding API Support for Filtering DAGs by Bundle Name and Version</h3><p>Issue Link: <a href="https://github.com/apache/airflow/issues/53739">https://github.com/apache/airflow/issues/53739</a></p><p>To address this, I first had to learn how to create DAG bundles so I could test the existing functionality. This was tricky, because as developers, we typically run Airflow using Breeze, and the documentation doesn’t always translate directly to that setup. You really need a solid understanding of Breeze to get this working.</p><p>After about two hours of troubleshooting, I managed to create two DAG bundles. Once that was done, I figured out how to create DAG bundle versions, and finally reached the point where I could test the API routes.</p><p>I implemented the required functionality in both the UI and public API routes. For anyone unfamiliar: in Airflow, there are two folders for API routes. The public routes are backward compatible and used by external consumers, while the UI routes are for the Airflow UI and don’t guarantee backward compatibility. I made changes to both, wrote tests, ran pre-commit hooks (which generated spec files and other outputs), and pushed my changes.</p><p>Here’s the link to my PR: <a href="https://github.com/apache/airflow/pull/54004">https://github.com/apache/airflow/pull/54004</a></p><p>I received some feedback asking me to add one more test. That turned out to be quite challenging, since the test itself was pretty complex. After some struggling, I finally figured it out and requested another review.</p><p>With my PR submitted, I polished up my demo slides for the final presentation and reviewed them with our pod leader. Our final demo is on Tuesday, and I’m really looking forward to it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6c57e0feb7e6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diary of an MLH SWE Fellow — Week 09]]></title>
            <link>https://medium.com/@aryank1511/diary-of-an-mlh-swe-fellow-week-09-cb6a5544c723?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb6a5544c723</guid>
            <category><![CDATA[apache-airflow]]></category>
            <category><![CDATA[mlh-fellowship]]></category>
            <category><![CDATA[open-source]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Fri, 25 Jul 2025 02:03:49 GMT</pubDate>
            <atom:updated>2025-07-25T02:03:49.950Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diary of an MLH SWE Fellow — Week 09</h3><h4>Tackling Complex API &amp; UI Changes in Apache Airflow</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H-IXaMWbb198GbB0zRlocQ.png" /></figure><h3>Introduction</h3><p>In my last blog, I mentioned I was hunting for bigger challenges in Apache Airflow, ideally wrapping up my term with acomplex issue. Well, mission accomplished, because I landed one that’s both fun and massive. It’s a mix of backend API changes and frontend UI updates, and it’s going to be my main focus for the next few weeks.</p><h3>Restoring Legacy Filters in Airflow’s New UI</h3><p>Here’s the background:<br>Apache Airflow’s frontend used to be built on Flask AppBuilder, but has now moved to a more modern ReactJS UI. While this upgrade is awesome, it came with a tradeoff, the new UI lost a few handy filters from the old DAGs (Directed Acyclic Graphs) view. The Airflow team wants to bring these filters back.</p><p>The API powering these filters also shifted, moving from Flask routes to FastAPI. Some of the routes required for these filters don’t exist yet. So, before we can build the filters in the UI, we first have to implement all the backend pieces!</p><p>If you want the full details, check out the main issue on GitHub:<br><a href="https://github.com/apache/airflow/issues/53041">Restore legacy filters in the new DAGs view</a></p><h3>My Plan of Action</h3><p>This project is essentially split into two main parts:</p><ol><li><strong>API work:</strong> Add all the required endpoints and filtering logic in the FastAPI backend.</li><li><strong>UI work:</strong> Once the backend is ready, wire up the new filters in the React frontend.</li></ol><p>To keep things manageable, I broke the main task into several sub-issues, each focused on a specific filter. Here are the sub issues that I have opened so far:</p><ul><li><strong>Add </strong><strong>has_import_errors filter to Core API </strong><strong>GET /dags endpoint:</strong><br><a href="https://github.com/apache/airflow/issues/53536">Issue #53536</a></li><li><strong>Add API support for filtering DAGs by timetable type:</strong><br><a href="https://github.com/apache/airflow/issues/53738">Issue #53738</a></li><li><strong>Add API support for filtering DAGs by bundle name and version:</strong><br><a href="https://github.com/apache/airflow/issues/53739">Issue #53739</a></li><li><strong>Enhance API support for filtering DAGs with asset-based schedules:</strong><br><a href="https://github.com/apache/airflow/issues/53740">Issue #53740</a></li><li><strong>Add API support for filtering unscheduled DAGs:</strong><br><a href="https://github.com/apache/airflow/issues/53741">Issue #53741</a></li></ul><p>The idea is to finish the API work for all these sub-issues first, then update the UI in one full swoop, since adding the frontend filters will mostly just be a matter of calling these new API endpoints.</p><h3>The first sub-issue did not go too well</h3><p>I dove in with the first issue, adding a has_import_errors filter. Seemed straightforward, but it wasn’t.</p><p><strong>Here’s what I discovered:</strong></p><ul><li>The dags table in the Airflow database has an has_import_errors field.</li><li>But if a DAG fails to import (e.g., a Python error in the DAG file), it doesn’t get added to the dags table at all.</li><li>Instead, failed DAGs are tracked in a completely separate table called import_error.</li><li>In the Airflow UI, these broken DAGs show up in a separate “Import Errors” section, not mixed in with valid DAGs.</li></ul><p><strong>Quick code snippet to show what I mean:</strong></p><pre>from airflow import DAG<br>from datetime import datetime</pre><pre># This line will break the import!<br>this_will_throw_an_error</pre><pre>default_args = {&#39;start_date&#39;: datetime(2023, 1, 1)}</pre><pre>dag = DAG(<br>    &#39;broken_dag&#39;,<br>    default_args=default_args,<br>    schedule_interval=None,<br>)</pre><p>If you try to add a broken DAG like this, it does not appear in the dags table. But you’ll see it in the UI’s Import Errors section, thanks to the import_error table.</p><p>I flagged this to the maintainers, since it means there’s no way to filter for has_import_errors in the dags table, the broken DAGs just aren’t there!</p><p><strong>Here’s what I asked:</strong></p><blockquote><em>Given this setup, it seems like adding an API/UI filter on </em><em>has_import_errors wouldn’t actually be useful, since the broken DAGs aren’t in the </em><em>dags table to be filtered in the first place, they’re already shown separately in the UI.</em></blockquote><blockquote><em>Should we still go ahead with this filter, or just close the issue as “not planned”? Happy to move on to the other filters in the meantime!</em></blockquote><p>The response:</p><blockquote><em>“Yeah, maybe just disregard that field for now and focus on other filters.<br>It’s weird , I wasn’t able to identify where that DAG attribute was set. I’ll need to do more digging.”</em></blockquote><p>So that settles it, this filter isn’t worth implementing right now. (If you’re curious, this is a pretty common scenario in open-source: you plan for something, dig in, and sometimes realize the original plan doesn’t make sense)</p><h3>What’s Next</h3><p>With that detour out of the way, I’m shifting focus to the other API filters I opened.</p><p><strong>My goal:</strong></p><ul><li>Week 10: Make serious progress on implementing these API routes.</li><li>Week 11: Continue API work and maybe start wiring up the UI.</li><li>Week 12: Finish the UI, tie everything together, and (hopefully!) have an awesome end-of-term demo to show off.</li></ul><p>Stay tuned for updates, I’ll share more details as I chip away at these features.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb6a5544c723" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diary of an MLH SWE Fellow — Week 07 and 08]]></title>
            <link>https://medium.com/@aryank1511/diary-of-an-mlh-swe-fellow-week-07-and-08-d2fc141f73a0?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/d2fc141f73a0</guid>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[mlh-fellowship]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[apache-airflow]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Wed, 16 Jul 2025 21:54:43 GMT</pubDate>
            <atom:updated>2025-07-16T21:54:43.802Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diary of an MLH SWE Fellow — Week 07 and 08</h3><h4>The Not-So-Glamorous Weeks of the Fellowship</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e_8Uw-eqpEMtQj25BxFI2g.png" /></figure><h3>Introduction</h3><p>Hey everyone! I’m rolling weeks 07 and 08 into one blog post because, honestly, things have been pretty slow on my Apache Airflow journey lately. Not a lot got done, so instead of forcing two lackluster updates, I figured I’d give you a real snapshot of the past couple weeks and set the stage for the final stretch.</p><p>We’re now kicking off week 09 of the 12-week MLH Fellowship, which means there are only four weeks left, including this one. My personal goal is to close out at least two more issues and get two more PRs merged before the end-of-program demo since I wanna finish strong.</p><h3>The Mid-Program Demo (And My Subway Saga)</h3><p>After a few reschedules, we finally had our mid-program demo with stakeholders from RBC (shoutout to them for sponsoring the project!). The demo went… alright. We walked through what we’ve built so far, highlighted two PRs we’re proud of, and talked about what we’ve learned as MLH fellows.</p><p>I had to present my part of the demo from a Subway joint (the sandwich place) because I had a health card appointment right after. My laptop connection dropped mid-demo, so I had to awkwardly ask the Subway employee for the WiFi password. Luckily, he hooked me up and I got to finish my presentation, but it wasn’t exactly the smoothest experience.</p><h3>Why the Slump?</h3><p>Honestly, these two weeks felt like a slump. Airflow’s a massive, slow-moving project, and lately, there just haven’t been many beginner-friendly or mid-level issues to pick up. I was also tied up with some other commitments, so my productivity on Airflow took a hit. All three of us working on Airflow for this fellowship felt like we didn’t have anything super “flashy” to show in the demo, which was a bit of a bummer.</p><h3>Scouting for Issues</h3><p>But I’m back and motivated! I spent some time digging through Airflow’s GitHub issues and found a few that look interesting. Here are the ones I commented on today:</p><ul><li><a href="https://github.com/apache/airflow/issues/53381">#53381</a></li><li><a href="https://github.com/apache/airflow/issues/52660">#52660</a></li></ul><p>I’m also eyeing another interesting one (<a href="https://github.com/apache/airflow/issues/52660">#52660</a>) and planning to chat with my mentor about it soon. Fingers crossed I can get something cool assigned to me so I can wrap up the fellowship on a high note.</p><h3>Wrapping Up</h3><p>So yeah, the past two weeks were pretty quiet, but I’m ramping back up. I’m committed to finishing off strong, tackling some fun new issues, and hopefully having something awesome to show at the final demo. Stay tuned!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d2fc141f73a0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MLH Fellowship: Reflections at the Halfway Point (Weeks 1–6)]]></title>
            <link>https://medium.com/@aryank1511/mlh-fellowship-reflections-at-the-halfway-point-weeks-1-6-28011c6bf5e9?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/28011c6bf5e9</guid>
            <category><![CDATA[mlh-fellowship]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[apache-airflow]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Tue, 01 Jul 2025 17:03:19 GMT</pubDate>
            <atom:updated>2025-07-01T17:03:19.878Z</atom:updated>
            <content:encoded><![CDATA[<h4>How six weeks of open-source, mentorship, and setbacks have shaped my path as an SWE Fellow</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cws_8sqcWd8C0Ja93kSVUw.jpeg" /></figure><p>Time really does fly! With Week 6 wrapped up and Week 7 underway, I’m officially halfway through my journey as an SWE Fellow in the MLH Fellowship. When I started back in May, July felt far off, but here we are.</p><p>The MLH Fellowship is a 12-week program where students contribute to open-source projects. For me, that project has been <a href="https://github.com/apache/airflow">Apache Airflow</a>, with sponsorship from <a href="https://www.rbcroyalbank.com/personal.html">RBC (Royal Bank of Canada)</a>. These past six weeks have been full of learning, building, and collaborating. In this post, I’m taking a step back to reflect on everything I’ve experienced so far.</p><h3>What I’ve Been Working On</h3><p>Since joining, I’ve really immersed myself in the Airflow ecosystem, learning how it works under the hood, navigating a huge codebase, and connecting with an amazing community of maintainers and contributors.</p><h4>Issues Tackled</h4><p>In total, I’ve picked up <strong>9 issues</strong> in the Airflow repo so far:</p><ul><li><strong>Closed:</strong> 4</li><li><strong>In Progress:</strong> 3</li><li><strong>Closed as Not Planned:</strong> 2 (admittedly, it stings a bit to see these closed after investing time digging through the code, but it’s all part of the open-source process)</li></ul><p>Here are the links to the issues I have worked on:</p><p><strong>Completed:</strong></p><ul><li><a href="https://github.com/apache/airflow/issues/50991">Add Deadline Alert to DAG Response</a></li><li><a href="https://github.com/apache/airflow/issues/51739">Typos in contributing-docs/11_documentation_building.rst</a></li><li><a href="https://github.com/apache/airflow/issues/52079">SnowflakeSqlApiOperator fails with JSONDecode at times</a></li><li><a href="https://github.com/apache/airflow/issues/52152">GlueJobHook.get_job_state doesn’t handle exceptions when fetching status</a></li></ul><p><strong>In Progress:</strong></p><ul><li><a href="https://github.com/apache/airflow/issues/51456">Airflow not writing logs to Elasticsearch</a></li><li><a href="https://github.com/apache/airflow/issues/51758">Add deadline order_by in list_dagruns</a></li><li><a href="https://github.com/apache/airflow/issues/52056">Incorrect timezone display in task log view</a></li></ul><p><strong>Closed as Not Planned:</strong></p><ul><li><a href="https://github.com/apache/airflow/issues/48735">Add Debug Logging in HTTP Provider</a></li><li><a href="https://github.com/apache/airflow/issues/22317">Make pause DAG its own role separate from edit DAG</a></li></ul><h4>Pull Requests</h4><p>On the PR front, I’ve raised a total of <strong>5 PRs</strong>:</p><ul><li><strong>Merged:</strong> 4</li><li><strong>Closed:</strong> 1 (because the related issue was closed as not planned)</li></ul><p>Here are links to my Pull Requests:</p><p><strong>Merged:</strong></p><ul><li><a href="https://github.com/apache/airflow/pull/51740">Fix typos in contributing-docs/11_documentation_building.rst</a></li><li><a href="https://github.com/apache/airflow/pull/51698">AIP-86: Add deadline to DagResponse</a></li><li><a href="https://github.com/apache/airflow/pull/52118">Add tests to test whether Snowflake SQL API handles invalid JSON</a></li><li><a href="https://github.com/apache/airflow/pull/52262">Handle exceptions when fetching status in GlueJobHook</a></li></ul><p><strong>Closed:</strong></p><ul><li><a href="https://github.com/apache/airflow/pull/51352">Providers/HTTP: Add Debug logging for improved troubleshooting in the HTTP Provider Hook</a></li></ul><h3>Reflection</h3><p>While working on open source has taught me a ton, I have to admit , it can get pretty lonely sometimes. When you get stuck and there’s nobody immediately around to help, it’s easy to feel like giving up on an issue. Luckily, in our case, we had mentors assigned to us who are actual Apache Airflow maintainers. Having someone to fall back on and ask questions really made a difference and kept me going whenever I hit a roadblock.</p><p>I think one thing that worked in my favor was spending a huge amount of time at the beginning just learning about the product and how to use it. I created sample DAGs, ran Airflow both as a developer and a user, and really tried to understand its capabilities. I remember binging YouTube videos from Airflow conferences where contributors talked about the architecture and how to get started, which helped a lot. I even took a Udemy course just to nail down the basics. All of this took longer than I expected, and honestly, it was pretty demoralizing when my first PR didn’t get merged, since we realized that feature wasn’t really needed. I’d worked on it for a week, only for it never to see the light of day.</p><p>Since then, I’ve faced similar situations, twice, in fact! I’ve learned that’s just the nature of open source. You might raise an issue or work on a feature thinking it’s valuable, but the project’s priorities might not align, or the change is too complicated for what’s needed, and it gets closed as “not planned.” Early on, I remember getting stuck on two issues at once and feeling really discouraged, by the end of week 4, I still had zero closed PRs or issues. But I kept pushing, and now, at the end of week 6, I have four under my belt.</p><p>Honestly, I’m not that proud of the specific changes I’ve made so far, because they haven’t been the highest-impact contributions. In fact, just today, another issue I’d invested a lot into, learning the whole auth manager flow and mechanism, was closed as “not planned.” That one was supposed to be my biggest contribution yet, and I was hoping to show it off in our upcoming midterm presentation with Airflow maintainers and RBC stakeholders.</p><p>But looking back, I realize all the time I spent learning and struggling in the first half was actually preparing me for what’s next. I’m now ready to tackle bigger, more challenging issues in the second half of the fellowship.</p><h3>My goal for the rest of the program</h3><p>My goal is to take on more responsibility, dig even deeper into the Airflow codebase, and make a real impact in the community.</p><p>The MLH Fellowship has been a lot of fun, and I’ve met some amazing people along the way. I’m looking forward to seeing how much I can grow in these next two months, and I hope to make some truly meaningful contributions soon.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=28011c6bf5e9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diary of an MLH SWE Fellow — Week 06]]></title>
            <link>https://medium.com/@aryank1511/diary-of-an-mlh-swe-fellow-week-06-3a2e31da4617?source=rss-edbdeda7e4a3------2</link>
            <guid isPermaLink="false">https://medium.com/p/3a2e31da4617</guid>
            <category><![CDATA[apache-airflow]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[mlh-fellowship]]></category>
            <dc:creator><![CDATA[Aryan Khurana]]></dc:creator>
            <pubDate>Sat, 28 Jun 2025 17:56:37 GMT</pubDate>
            <atom:updated>2025-06-28T17:56:37.277Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diary of an MLH SWE Fellow — Week 06</h3><h4>Two more PRs Merged</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OllP0sSeLbunQyPiB0iijQ.png" /></figure><h3>Introduction</h3><p>I mentioned in my last blog that I was finally gaining momentum, and contributing to Airflow is starting to feel genuinely fun. This week, I picked up two more issues, raised PRs for both, and successfully got them merged. That brings my total to <strong>4 merged PRs</strong> in the MLH Fellowship so far, which is great news because MLH typically expects at least 3–4 merged pull requests by the end of the program. So now, let’s talk about the issues!</p><h3>Issue 1: Snowflake JSON Decode Error</h3><p>Link: <a href="https://github.com/apache/airflow/issues/52079">https://github.com/apache/airflow/issues/52079</a></p><p>The first issue I tackled was relatively straightforward. Some providers in Airflow, like the Snowflake SQL API, fetch JSON responses from APIs. However, when the APIs fail, users were encountering unhandled JSON decode errors, and there was no retry mechanism in place.</p><p>This issue requested better error handling and a retry mechanism using the tenacity library. I dove into the problem, only to realize that someone else had already indirectly fixed it in a different PR: <a href="https://github.com/apache/airflow/pull/51463">#51463</a>.</p><p>Since their fix wasn’t directly tied to this issue, there was no test coverage verifying that it resolved the problem. So I wrote a couple of test cases to validate the behavior and raised my own PR which was eventually merged and officially closed the issue.</p><p>Link to my PR: <a href="https://github.com/apache/airflow/pull/52118">https://github.com/apache/airflow/pull/52118</a></p><h3>Issue 2: AWS Glue Retry and Error Handling</h3><p>Link: <a href="https://github.com/apache/airflow/issues/52152">https://github.com/apache/airflow/issues/52152</a></p><p>After that, one of the maintainers asked me if I’d be interested in working on a similar issue, this time involving the AWS Glue provider. I agreed and took it on. Even though it was pretty straightforward but it was the most complex issue I’ve worked on so far since before this my PRs didn’t have a lot of code changes.</p><p>This issue required actual logic changes, so I used what I learned from the previous issue and added retry mechanisms, proper error handling, and test coverage to ensure everything worked as expected.</p><p>Thanks to the experience I’ve gained, the development workflow felt much smoother this time. I’ve gotten used to:</p><ul><li>Spinning up the dev environment using Breeze</li><li>Making code changes</li><li>Running relevant tests with pytest</li><li>Running the pre-commit hooks</li><li>Committing and pushing changes and eventually raising a PR following best practices.</li></ul><p>Link to my PR: <a href="https://github.com/apache/airflow/pull/52262">https://github.com/apache/airflow/pull/52262</a></p><h3>Conclusion</h3><p>I didn’t mention it in this blog, but I did pick up another issue and spent some time working on it. Unfortunately, I hit a roadblock, so I’ll save the details for a future post once I figure out a solution. That said, I’m planning to take on more challenging issues moving forward. We’re officially halfway through the fellowship, and I’m excited to see what I can accomplish in the second half!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3a2e31da4617" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>