graph TD
    Approve["Member approved<br/>→ 'publish my card' flag set"]
    Approve --> Job["Minute job wakes up<br/>sees the flag"]
    Approve --> Tap["Member taps<br/>'confirm' in DM"]
    Job --> CheckA["Card posted yet?<br/>No"]
    Tap --> CheckB["Card posted yet?<br/>No"]
    CheckA --> PostA["Post card + notice"]
    CheckB --> PostB["Post card + notice"]
    PostA --> Dup["Two cards, two notices"]
    PostB --> Dup

The owner sent a screenshot: a returning member had just rejoined the group, and the bot had announced her profile card twice, two identical notices one above the other, same minute.

The log said it happened once

The journal had exactly one line: “Published approved profile card.” One publish and one success, but two announcements. That log line was the first wrong lead. It comes from the scheduled job and only the scheduled job, so any other code path that publishes a card does so silently.

Reading the group’s history directly told the real story: not just two notices, but two profile cards in the cards topic, all four messages within the same second.

Two doors into the same room

When someone is approved, two things can publish their card:

  1. A background job that runs once a minute and posts cards for anyone the approval bot has flagged.
  2. The member’s own “yes, that’s me” button, tapped after they join, which also publishes the card (and then opens up their write access).

Normally these are minutes apart and the second one finds the card already there and just edits it. This time the member tapped the button at the exact second the job fired. The bot handles updates concurrently, so both ran at once. Each one asked “is there a card yet?”, both got “no”, and each posted a card and an announcement. A textbook check-then-act race.

The fix

One lock per member around the publish function. The second caller now waits, re-reads the profile, finds the card the first caller just posted, and edits it instead of posting another.

The test mattered more than the fix: a fake bot with small artificial delays, firing two publishes at once. With the lock it produces one card and one notice. With the lock bypassed it produces two cards and two notices, exactly what the screenshot showed. A test that can’t reproduce the bug proves nothing about the fix. This one can.

Then cleanup: deleted the extra card and the extra notice, and kept the copies the database points to (and the one with a welcome reply already under it).

What I’d take from it

  • A single log line isn’t a single event. If only one of several code paths logs, the log only counts that path.
  • “Check, then act” across an await is a race the moment updates run concurrently, and a background job plus a human button is the classic pair. This bot has had three different duplicate-card bugs now. The first two were about edits failing. This one was about timing.
  • Look at the output, not the reported symptom. The report was “two notices”. The real damage was two cards, which only showed up by reading what was actually in the group.