<?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: Hilda Enyioko</title>
    <description>The latest articles on DEV Community by Hilda Enyioko (@hilda_enyioko).</description>
    <link>https://dev.to/hilda_enyioko</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%2F2902951%2Fbbd8b011-b0ba-4af7-981c-660c63e41f33.jpg</url>
      <title>DEV Community: Hilda Enyioko</title>
      <link>https://dev.to/hilda_enyioko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hilda_enyioko"/>
    <language>en</language>
    <item>
      <title>Two threads, one card, one invoice: hunting a TOCTOU bug in a payments codebase</title>
      <dc:creator>Hilda Enyioko</dc:creator>
      <pubDate>Mon, 03 Aug 2026 08:27:20 +0000</pubDate>
      <link>https://dev.to/hilda_enyioko/two-threads-one-card-one-invoice-hunting-a-toctou-bug-in-a-payments-codebase-1pb8</link>
      <guid>https://dev.to/hilda_enyioko/two-threads-one-card-one-invoice-hunting-a-toctou-bug-in-a-payments-codebase-1pb8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;TheeInsurance is a headless, API-first insurance platform for Nigeria. Insurance providers and distributors use it to manage plans, subscriptions, KYC, and payments through a multi-tenant Django REST backend. It integrates Interswitch Quickteller Pay and Nomba, including tokenized card storage for automated policy renewals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The bug lives in &lt;code&gt;charge_policy_renewal()&lt;/code&gt;.&lt;br&gt;
This function is responsible for auto-charging a customer's stored card when their policy is due for renewal. Since renewals can be triggered by the scheduler, an n8n renewal call, or a dunning retry, concurrent execution can legitimately happen under production load or after retry timing overlaps.&lt;/p&gt;

&lt;p&gt;To prevent double-charging, the function checked for an in-flight renewal before creating a new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payment_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAYMENT_TYPE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RENEWAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payment_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAYMENT_STATUS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GATEWAY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NOMBA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;in_flight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PaymentError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A renewal charge is already in progress.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db_transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks like a reasonable guard. But the &lt;code&gt;.exists()&lt;/code&gt; check and the &lt;code&gt;.create()&lt;/code&gt; are two separate operations, not covered by the same lock. If two callers hit this function within the same tiny window, both can evaluate &lt;code&gt;in_flight = False&lt;/code&gt; before either has committed its new &lt;code&gt;PENDING&lt;/code&gt; Transaction. Both then proceed to call Nomba's tokenized charge endpoint. Same card, same policy, two charges.&lt;/p&gt;

&lt;p&gt;This is a textbook TOCTOU (time-of-check to time-of-use) race condition. In a payments codebase like this one, it's not an edge case worth shrugging off. It's a customer getting charged twice for the same policy, with the operational cost of a refund and a support ticket on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Full PR: &lt;strong&gt;[&lt;a href="https://github.com/Hilda-Enyioko/theeinsurance_infrastructure/pull/8" rel="noopener noreferrer"&gt;https://github.com/Hilda-Enyioko/theeinsurance_infrastructure/pull/8&lt;/a&gt;]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reproduction test: &lt;code&gt;payments/tests/test_renewal_race_condition.py&lt;/code&gt; fires two concurrent calls to &lt;code&gt;charge_policy_renewal()&lt;/code&gt; for the same subscription, using a &lt;code&gt;threading.Barrier&lt;/code&gt; to force the interleaving deterministically (relying on raw OS thread timing would make a race-condition test flaky and unconvincing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before the fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: Expected exactly 1 successful renewal charge, got 2. Blocked: 0.
Race condition allowed duplicate billing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;10/10 manual runs reproduced the duplicate charge.&lt;/p&gt;

&lt;p&gt;BEFORE FIX TEST RESULT: &lt;a href="https://github.com/Hilda-Enyioko/theeinsurance_infrastructure/blob/main/race_condition_results.txt" rel="noopener noreferrer"&gt;race_condition_results.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After the fix:&lt;/strong&gt;&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="na"&gt;successes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1, blocked&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1, errors&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;renewal_txn_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0/10 runs produced a duplicate charge.&lt;/p&gt;

&lt;p&gt;AFTER FIX TEST RESULT: &lt;a href="https://github.com/Hilda-Enyioko/theeinsurance_infrastructure/blob/main/sentry_race_test_output.txt" rel="noopener noreferrer"&gt;sentry_race_test_output.txt&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;I fixed this at two layers, because I wanted both the &lt;em&gt;correct&lt;/em&gt; fix and a &lt;em&gt;guaranteed&lt;/em&gt; fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Close the race at the application level.&lt;/strong&gt; The subscription row is now locked with &lt;code&gt;select_for_update()&lt;/code&gt; at the very top of the function. Since every renewal targets a single subscription, locking that row naturally serializes renewal attempts for the same policy without reducing concurrency across unrelated subscriptions. The entire check-then-act sequence, status checks, the in-flight query, and the &lt;code&gt;Transaction.objects.create()&lt;/code&gt;, happens inside that one &lt;code&gt;atomic()&lt;/code&gt; block:&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;with&lt;/span&gt; &lt;span class="n"&gt;db_transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PolicySubscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&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="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... validation ...
&lt;/span&gt;    &lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;in_flight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PaymentError&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="c1"&gt;# lock released here — the Nomba HTTP call happens outside the transaction
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second concurrent call now blocks on the row lock until the first transaction commits, then correctly sees the in-flight renewal and backs off. I deliberately kept the outbound Nomba call &lt;em&gt;outside&lt;/em&gt; the atomic block. This is because holding a row lock during a 30-second-timeout network call would create its own problems (blocking legitimate concurrent reads on that subscription for the duration of a slow gateway call).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Add a database-level guarantee as a second line of defense.&lt;/strong&gt; Application logic can have bugs, or a future code path might bypass this function entirely. So I added a partial unique constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transaction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;constraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subscription&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;payment_type&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;gateway&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique_pending_renewal_per_subscription&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if the application-level lock is ever removed or bypassed, Postgres itself now refuses to store a second concurrent &lt;code&gt;PENDING&lt;/code&gt; renewal row for the same subscription. I think of the row lock as the "correct" fix and the constraint as the "can never regress" fix.&lt;/p&gt;

&lt;p&gt;The trickiest part of this whole exercise honestly was writing a test that could reliably &lt;em&gt;prove&lt;/em&gt; the race existed in the first place, given that race conditions are timing-dependent by nature. Using a &lt;code&gt;threading.Barrier&lt;/code&gt; patched into the &lt;code&gt;.filter()&lt;/code&gt; call let me force both threads to reach the vulnerable window at the same instant, every time, instead of hoping for an unlucky interleaving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;I instrumented &lt;code&gt;charge_policy_renewal()&lt;/code&gt; with a Sentry transaction and spans around each meaningful step so the fix is observable in production behavior:&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;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;renewal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charge_policy_renewal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sentry_txn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db_transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select_for_update subscription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PolicySubscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&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="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;in_flight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;renewal.duplicate_blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocked duplicate renewal charge attempt for subscription &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&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;raise&lt;/span&gt; &lt;span class="nc"&gt;PaymentError&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the concurrency test against this instrumented version, I could see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Performance trace&lt;/strong&gt; for &lt;code&gt;charge_policy_renewal&lt;/code&gt; showing the &lt;code&gt;db.lock&lt;/code&gt; span. The second thread's wait time on the row lock is directly visible in the waterfall.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;warning-level Issue&lt;/strong&gt; ("Blocked duplicate renewal charge attempt...") firing exactly once, from the thread that correctly got blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fht3q72f43tbj5276cmb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fht3q72f43tbj5276cmb0.png" alt="Issues tab showing the warning event" width="799" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmqkq0wsqwozzqig6v7ot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmqkq0wsqwozzqig6v7ot.png" alt="Performance tab showing the trace waterfall" width="799" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I set &lt;code&gt;send_default_pii=False&lt;/code&gt; given this codebase handles KYC and payment data.&lt;br&gt;
I'd dial &lt;code&gt;traces_sample_rate&lt;/code&gt; down from &lt;code&gt;1.0&lt;/code&gt; before this runs against real production traffic. 100% tracing on every payment call is unnecessary overhead at scale, &lt;code&gt;1.0&lt;/code&gt; was just useful for capturing this demo cleanly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;After identifying and fixing the race condition manually, I ran the original failing test's traceback and the pre-fix function source through Gemini as an independent sanity check on my root-cause reasoning:&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;google.generativeai&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;

&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOGLE_AI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GenerativeModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.0-flash&lt;/span&gt;&lt;span class="sh"&gt;"&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Given this test failure and the relevant function source, identify the
root cause and suggest a fix:

FAILURE:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;failure_traceback&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

FUNCTION:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;charge_policy_renewal_source&lt;/span&gt;&lt;span class="si"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemini independently converged on the same diagnosis: a TOCTOU gap between the &lt;code&gt;.exists()&lt;/code&gt; check and the &lt;code&gt;Transaction.create()&lt;/code&gt; call, with a &lt;code&gt;select_for_update()&lt;/code&gt;-based lock as the standard fix. I ran this as a standalone diagnostic script not as part of the application itself. I took this route because this is a payments code path. I didn't want to add an external AI dependency or extra latency to a real charge flow.&lt;/p&gt;




&lt;p&gt;Thanks for organizing the challenge. It was a great excuse to revisit an unfinished project. And, it ended up uncovering a race condition that could have resulted in real customers being charged twice. Those are exactly the kinds of bugs worth fixing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>infrastructureascode</category>
      <category>architecture</category>
    </item>
    <item>
      <title>"It's Not Working" Is Not an Error</title>
      <dc:creator>Hilda Enyioko</dc:creator>
      <pubDate>Sat, 01 Aug 2026 21:31:23 +0000</pubDate>
      <link>https://dev.to/hilda_enyioko/its-not-working-is-not-an-error-26p4</link>
      <guid>https://dev.to/hilda_enyioko/its-not-working-is-not-an-error-26p4</guid>
      <description>&lt;p&gt;Three weeks ago, the frontend developer on a team I manage reported an error she was facing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's not working!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two days ago, another developer at work reported an error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"500 Internal Server Error..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No screenshot. No error message. No endpoint. No timestamp.&lt;br&gt;
Just four words can mean anything, like a user forgetting to click "Save."&lt;/p&gt;

&lt;p&gt;I've worked across multiple projects as a backend engineer, and I currently lead the build team behind FUTO Aid, our flagship student fundraising platform at IDC-FUTO. Leading a cross-functional team means I spend almost every day at the intersection of frontend and backend development.&lt;/p&gt;

&lt;p&gt;One recurring friction point I've noticed is that frontend developers, who understand how software systems work, often report bugs the way end users do.&lt;/p&gt;

&lt;p&gt;It isn't necessarily because they don't know better. More often, no one has shown them what good error reporting looks like or why it matters.&lt;/p&gt;

&lt;p&gt;This article isn't a rant. It's the guide I wish existed the first time I sat down with a teammate and said, "I can't fix what I can't see."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Proper Error Reporting Matter?
&lt;/h2&gt;

&lt;p&gt;When a bug is reported vaguely, the backend engineer becomes a detective before they can become a problem-solver.&lt;/p&gt;

&lt;p&gt;For every vague reporting, we have the same extensive loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What were you doing when it happened?"&lt;/li&gt;
&lt;li&gt;"Can you send a screenshot?"&lt;/li&gt;
&lt;li&gt;"Which page/button/action?"&lt;/li&gt;
&lt;li&gt;"Is this still happening?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before the real debugging starts, there's already an entire slack thread, or a 10-min (could be longer) call trying to reproduce the issue reported. Multiply that across a sprint, across a team, across a project timeline. It adds up to real, avoidable delay.&lt;/p&gt;

&lt;p&gt;Good error reporting is a force multiplier. It's often the difference between a bug fixed in 15 minutes and one that takes up half a sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes a Developer's Error Report Different From a User's?
&lt;/h2&gt;

&lt;p&gt;I like to tell engineers on my team that while end users describe symptoms, developers should report evidence.&lt;/p&gt;

&lt;p&gt;An end user is allowed to say "it's not working." They don't have access to dev tools, they don't know what an endpoint is, and they shouldn't have to.&lt;/p&gt;

&lt;p&gt;A developer reporting a bug, especially to another developer, has no such excuse. Precision and clarity is the key differentiator.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes an Error Report Developer-Level Precise and Clear?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Copy the Exact Error Message:
&lt;/h4&gt;

&lt;p&gt;This isn't the UI-wrapped error. This is the literal text from the literal error from the browser console, copied — not paraphrased. Paraphrasing an error message makes it lose its 'stew'. It loses the specific detail (error codes, stack traces, variable names) that often points straight to the cause.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Get the Failing Endpoint:
&lt;/h4&gt;

&lt;p&gt;Which API call actually failed? This is visible in the Network tab. Rather than guessing across an entire codebase, it immediately tells the backend engineer which controller, service, or route to look at.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Note The Exact Time or Time Range:
&lt;/h4&gt;

&lt;p&gt;This is a crucial part of reporting to help the backend engineer easily trace the logs on server-side produced by the failing endpoint. It is important to note that, "It happened this morning" is not a timestamp.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Copy the Request and Response Details:
&lt;/h4&gt;

&lt;p&gt;The status code (400? 401? 500?), the request payload, and the response body are all visible in the Network tab. A good 70% of the time, they reveal why something failed, not just that it failed.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Provide Steps to Reproduce:
&lt;/h4&gt;

&lt;p&gt;What sequence of actions led to the error? Can it be triggered consistently, or was it a one-off?&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Share What's Already Been Tried:
&lt;/h4&gt;

&lt;p&gt;This one matters more between developers than anywhere else. If you're a backend developer reporting an issue to another backend developer, saying "I tried restarting the queue worker and clearing the Redis cache, still fails" saves your teammate from repeating dead ends.&lt;/p&gt;

&lt;h4&gt;
  
  
  7. Provide Environment Context:
&lt;/h4&gt;

&lt;p&gt;Local, staging, or production? Which branch or deployment? A bug that only happens in staging because of a stale environment variable looks very different from one that's broken everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief Walkthrough: Using the Network Tab to Report Errors Properly
&lt;/h2&gt;

&lt;p&gt;If there's one browser tool every frontend developer should be fluent in before reporting a bug to a backend engineer, it's the &lt;strong&gt;Network tab&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's a simple walkthrough:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6molrergnv2cdqike1xy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6molrergnv2cdqike1xy.png" alt="Network Tab on Chrome Browser" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open dev tools&lt;/strong&gt; (&lt;code&gt;F12&lt;/code&gt; or right-click → Inspect) and go to the &lt;strong&gt;Network&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce the issue&lt;/strong&gt; — perform the action that triggers the bug while the Network tab is recording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the failing request.&lt;/strong&gt; Failed requests are usually easy to spot — they'll show a red status or a 4xx/5xx status code in the Status column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click on that request.&lt;/strong&gt; This opens a detail panel with several useful tabs:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headers&lt;/strong&gt; — shows the endpoint URL, request method (GET, POST, etc.), and status code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload/Request&lt;/strong&gt; — shows exactly what data was sent to the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response&lt;/strong&gt; — shows exactly what the server sent back, often including a specific error message from the backend itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing&lt;/strong&gt; — shows how long the request took, useful for diagnosing timeouts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy, don't summarize.&lt;/strong&gt; Copy the request URL, the response body, and the status code directly into your bug report. A screenshot of this panel works too — as long as all four pieces (URL, method, status, response) are visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check the Console tab&lt;/strong&gt; for any related JavaScript errors that occurred alongside the failed request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've never used the Network tab while reporting a bug, this habit alone will improve the quality of your reports overnight.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Good bug report&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;POST /api/user/avatar-upload returned &lt;strong&gt;413 Payload Too Large&lt;/strong&gt; at &lt;strong&gt;2:14 PM&lt;/strong&gt;.&lt;br&gt;
The request payload exceeded the upload size limit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This single sentence tells the backend engineer exactly where to look, cutting the back-and-forth down to zero.&lt;/p&gt;




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

&lt;p&gt;None of this is complicated. It's not asking frontend developers to understand backend architecture, or expecting anyone to debug someone else's code. It's just asking that reports come from a developer's vantage point.&lt;/p&gt;

&lt;p&gt;Small habits like this compound. They reduce the back-and-forth between developers and development units, cut down the time spent debugging and deciphering vague reports, and, maybe most importantly, they build a culture of precision within a team. On teams like IDC-FUTO Build Team, where everyone's time is already stretched thin, that precision isn't a nice-to-have. It's what keeps a project moving.&lt;/p&gt;

&lt;p&gt;I'd honestly love to see developers do better when communicating issues.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>discuss</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
