DB Olympics

Queries · q346 · VP Platform Engineering · week 2, night 4

Payment Attempts During the Nov 28 2025 Outage Window

“Did payment processing itself fail or stall during the same 14:00-16:00 UTC window, confirming a full checkout-path outage rather than just the reminder-email job?”

Manager · asked the questionDiego MoralesVP Platform Engineering. Former reliability lead at a payments company. Owns uptime and the integrations merchants depend on.
Analyst · planned the reportLucas MoreauPragmatic: answers the question asked with the simplest reliable report.
Engineer · wrote the SQLFelixJunior engineer who nests subqueries in FROM and WHERE (IN, EXISTS, correlated subqueries) rather than CTEs.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. A business event lands on the VP Platform Engineering's desk

    Week 1; shared with Samuel Whitcombe, Head of Merchant Success

    Leadership wants a review of platform reliability in 2025: failed and slow background jobs, health check failures, rate limiting, webhook delivery problems, and which merchants felt them.

  2. Diego Morales writes to Lucas Moreau

    The manager, at the start of week 2

    Subject: Re: 2025 Platform Reliability Review — good start, a few things I need tightened up

    Lucas,

    Thanks — this is useful and more honest than I expected on the limits of what we can see. A few things stood out and need more work before Friday's memo goes to leadership.

    First, the retention gap is itself a finding, not just a caveat. We're flying with 90 days of job history and 60 days of health-check history — that means we genuinely don't know how 2025 looked before October. I want that stated plainly and separately in the memo as a reliability/observability gap in its own right, not buried as a footnote under the carts.remind story.

    Second, the ERP/WMS/Accounting webhook stuck-retry pattern is, to me, the most important and actionable thing in here — more than the one-week carts.remind incident. I want that properly validated and sized, not eyeballed, before we say anything about it to leadership.

    Questions for this week:

    1. Can you redo the webhook comparison correctly and tell me: how many merchants are affected by this stuck-retry pattern, and what's the revenue or order volume behind them? I want to know if this is a handful of our biggest accounts or a long tail.
    2. Has the ~15% stuck-retry rate on those sync webhooks been happening all year, or is it new/recent? Even without full job history, is there any way to tell whether this is a longstanding bug we've lived with or something that regressed recently?
    3. On carts.remind — you said it looks tied to a feature flag or config rather than load. Can we get more confidence on that? I want to know if this is a one-off we already fixed, or a latent risk that could recur with other job types.
    4. Setting aside strict proof of causation, can you give me your best read on whether the ticket-volume climb for the 10 affected tenants in the weeks before the failures showed up in our logs is something merchants were telling us about directly (i.e., check if their tickets mention sync/reminders/missing carts) — even a rough read would tell us if our support channel was effectively an early warning system we ignored.

    On rate limiting: agreed, drop it for this cycle — don't spend a slot trying to force an answer out of config data alone. Just flag it as a 2026 instrumentation ask, as you already have.

    Priority for your remaining slots: #1 and #2 above. The webhook/sync issue is the one I can take into a fix-it conversation with engineering next week if the numbers hold up — I'd rather have that nailed down than more polish on the carts.remind narrative.

    Diego

  3. Lucas Moreau plans the night

    The analyst · week 2, night 4, request 3 of 3

    Report 1 confirms health checks for checkout/storefront run only every 2 hours (checks exist at 12:00, 14:00, 16:00 but nothing in between) - so even though monitoring did catch a 100%-unhealthy reading at 14:00, our detection granularity for this path is coarse (up to 2 hours to notice), which is itself worth flagging for Diego's #3. Report 2 is the smoking gun on business impact: hour 14 had just 1 order and hour 15 has NO row at all (zero orders) on Black Friday, versus 7-11 orders in every neighboring hour that day - a near-total order freeze for roughly 2 hours on the year's biggest shopping day. Report 3 shows Black Friday 2025 total revenue (€40,166) came in 26% below Black Friday 2024 (€54,186) despite the business having grown since then, which is directionally consistent with real lost sales, though other factors could contribute and I haven't isolated the outage's exact share of that gap. This is my last night before the memo, so I'm using the 3 slots to: (1) precisely size the lost-order/revenue window by comparing the same 14:00-16:00 UTC slot across Nov 28 against two baseline Fridays and Black Friday 2024, so I can give Diego an actual number rather than an impression; (2) check whether checkout/storefront ever showed another severe (>=50% unhealthy) outage anywhere else in the Nov-Dec window we have, to answer whether this was a true one-off or a recurring latent risk; (3) check payment attempts during the same window, since a checkout-path outage should show up there too and would confirm this was a full checkout failure, not just a reminder-email glitch.

    Payment Attempts During the Nov 28 2025 Outage Window
    Question
    Did payment processing itself fail or stall during the same 14:00-16:00 UTC window, confirming a full checkout-path outage rather than just the reminder-email job?
    One row per
    hour of the day, for 2025-11-28 (UTC)
    Columns
    • hour
    • total_payment_attempts
    • captured_or_authorized_count
    • failed_count
    • failure_rate_pct
  4. Felix writes the SQL

    The engineer · 1 dry run on the empty schema, then the submission

    Dry run 1 · STATUS: COMPILES (dry run on the empty schema, no data read)
    SELECT
      EXTRACT(HOUR FROM pa.attempted_at) AS hour,
      COUNT(*) AS total_payment_attempts,
      COUNT(*) FILTER (WHERE pa.status IN ('authorized', 'captured')) AS captured_or_authorized_count,
      COUNT(*) FILTER (WHERE pa.status = 'failed') AS failed_count,
      ROUND(
        COUNT(*) FILTER (WHERE pa.status = 'failed')::decimal / NULLIF(COUNT(*), 0) * 100,
        2
      ) AS failure_rate_pct
    FROM payment_attempts pa
    WHERE pa.attempted_at >= DATE '2025-11-28'
      AND pa.attempted_at < DATE '2025-11-29'
    GROUP BY EXTRACT(HOUR FROM pa.attempted_at)
    ORDER BY hour ASC

    “Query compiles cleanly and matches the requested grain/columns.”

    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. The query buckets payment_attempts by UTC hour for 2025-11-28, computing total attempts, captured/authorized counts, failed counts, and failure rate — across all tenants, since the outage question concerns the platform's checkout path as a whole rather than any single merchant. Any hour with zero attempts will simply not appear in the printed rows, as anticipated in the request.”

  5. The report comes back overnight

    As Lucas Moreau saw it, from the small dataset the agents worked on

    hour | total_payment_attempts | captured_or_authorized_count | failed_count | failure_rate_pct
    -----+------------------------+------------------------------+--------------+-----------------
       0 |                      4 |                            4 |            0 |              0.0
       1 |                      5 |                            5 |            0 |              0.0
       2 |                      2 |                            2 |            0 |              0.0
       3 |                      2 |                            1 |            1 |             50.0
       4 |                      4 |                            4 |            0 |              0.0
       5 |                      2 |                            2 |            0 |              0.0
       6 |                      6 |                            5 |            1 |            16.67
       7 |                      4 |                            3 |            1 |             25.0
       8 |                      9 |                            8 |            1 |            11.11
       9 |                     13 |                           12 |            1 |             7.69
      10 |                      3 |                            3 |            0 |              0.0
      11 |                      7 |                            7 |            0 |              0.0
      12 |                     15 |                           15 |            0 |              0.0
      13 |                     14 |                           14 |            0 |              0.0
      14 |                      3 |                            3 |            0 |              0.0
      15 |                      2 |                            2 |            0 |              0.0
      16 |                     17 |                           13 |            4 |            23.53
      17 |                     11 |                           11 |            0 |              0.0
      18 |                      9 |                            8 |            1 |            11.11
      19 |                     18 |                           17 |            1 |             5.56
      20 |                     17 |                           17 |            0 |              0.0
      21 |                     18 |                           18 |            0 |              0.0
      22 |                     13 |                           12 |            1 |             7.69
      23 |                      6 |                            6 |            0 |              0.0
    (24 rows)
  6. What came of it

    The memo and the queries that followed; links found by title, so inferred

    It builds on:

  7. It becomes benchmark query q346

    Changed only where the benchmark needs the same result on every system

    • spellingcast
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision; bare NUMERIC cast that only served the rounding removed
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results
    The benchmark text, against the submitted SQL
    SELECT  EXTRACT(HOUR FROM pa.attempted_at) AS hour,  COUNT(*) AS total_payment_attempts,  COUNT(*) FILTER (WHERE pa.status IN ('authorized', 'captured')) AS captured_or_authorized_count,  COUNT(*) FILTER (WHERE pa.status = 'failed') AS failed_count,  ROUND(    COUNT(*) FILTER (WHERE pa.status = 'failed')::decimal / NULLIF(COUNT(*), 0) * 100,    2  ) AS failure_rate_pct  CAST(COUNT(*) FILTER (WHERE pa.status = 'failed') AS decimal(18,6)) / NULLIF(COUNT(*), 0) * 100 AS failure_rate_pctFROM payment_attempts paWHERE pa.attempted_at >= DATE '2025-11-28'  AND pa.attempted_at < DATE '2025-11-29'GROUP BY EXTRACT(HOUR FROM pa.attempted_at)ORDER BY hour ASCORDER BY hour ASC NULLS LAST
    SELECT
      EXTRACT(HOUR FROM pa.attempted_at) AS hour,
      COUNT(*) AS total_payment_attempts,
      COUNT(*) FILTER (WHERE pa.status IN ('authorized', 'captured')) AS captured_or_authorized_count,
      COUNT(*) FILTER (WHERE pa.status = 'failed') AS failed_count,
      CAST(COUNT(*) FILTER (WHERE pa.status = 'failed') AS decimal(18,6)) / NULLIF(COUNT(*), 0) * 100 AS failure_rate_pct
    FROM payment_attempts pa
    WHERE pa.attempted_at >= DATE '2025-11-28'
      AND pa.attempted_at < DATE '2025-11-29'
    GROUP BY EXTRACT(HOUR FROM pa.attempted_at)
    ORDER BY hour ASC NULLS LAST
    1 ms10 ms100 msHyper22.6 msDuckDB5.0 msGizmoSQL7.3 msclickhouse_brew14.8 msFirebolt22.2 msDataFusion41.2 msClickHouse15.2 ms
  8. Everything, as recorded

    The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted

    Felix's whole session, 7 turns
    • The request, as Felix received it
      Report request from Lucas Moreau (analyst for the VP Platform Engineering):
      
      Title: Payment Attempts During the Nov 28 2025 Outage Window
      Question: Did payment processing itself fail or stall during the same 14:00-16:00 UTC window, confirming a full checkout-path outage rather than just the reminder-email job?
      Grain: one row per hour of the day, for 2025-11-28 (UTC)
      Columns:
      - hour
      - total_payment_attempts
      - captured_or_authorized_count
      - failed_count
      - failure_rate_pct
      Details: Table payment_attempts, filtered to attempted_at on 2025-11-28 (all 24 hours UTC). Group by hour of attempted_at. captured_or_authorized_count = count where status in ('authorized','captured'); failed_count = count where status='failed'; failure_rate_pct = failed_count / total_payment_attempts * 100, rounded 2dp. Order by hour ascending (0-23); hours with zero attempts simply won't appear, note the gap if any in the report.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Felix calls dry_run
      SELECT
        EXTRACT(HOUR FROM pa.attempted_at) AS hour,
        COUNT(*) AS total_payment_attempts,
        COUNT(*) FILTER (WHERE pa.status IN ('authorized', 'captured')) AS captured_or_authorized_count,
        COUNT(*) FILTER (WHERE pa.status = 'failed') AS failed_count,
        ROUND(
          COUNT(*) FILTER (WHERE pa.status = 'failed')::decimal / NULLIF(COUNT(*), 0) * 100,
          2
        ) AS failure_rate_pct
      FROM payment_attempts pa
      WHERE pa.attempted_at >= DATE '2025-11-28'
        AND pa.attempted_at < DATE '2025-11-29'
      GROUP BY EXTRACT(HOUR FROM pa.attempted_at)
      ORDER BY hour ASC
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: hour, total_payment_attempts, captured_or_authorized_count, failed_count, failure_rate_pct
    • FelixQuery compiles cleanly and matches the requested grain/columns.
    • Felix calls submit
      SELECT
        EXTRACT(HOUR FROM pa.attempted_at) AS hour,
        COUNT(*) AS total_payment_attempts,
        COUNT(*) FILTER (WHERE pa.status IN ('authorized', 'captured')) AS captured_or_authorized_count,
        COUNT(*) FILTER (WHERE pa.status = 'failed') AS failed_count,
        ROUND(
          COUNT(*) FILTER (WHERE pa.status = 'failed')::decimal / NULLIF(COUNT(*), 0) * 100,
          2
        ) AS failure_rate_pct
      FROM payment_attempts pa
      WHERE pa.attempted_at >= DATE '2025-11-28'
        AND pa.attempted_at < DATE '2025-11-29'
      GROUP BY EXTRACT(HOUR FROM pa.attempted_at)
      ORDER BY hour ASC
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • FelixSubmitted. The query buckets `payment_attempts` by UTC hour for 2025-11-28, computing total attempts, captured/authorized counts, failed counts, and failure rate — across all tenants, since the outage question concerns the platform's checkout path as a whole rather than any single merchant. Any hour with zero attempts will simply not appear in the printed rows, as anticipated in the request.