skills / code

postmortem-writing

what it does

Write effective blameless postmortems with root cause analysis, timelines, and action items. Use when conducting incident reviews, writing postmortem documents, or improving incident response processes.

Verified Before and after: 2 models 1 try per case View source Download .zip

Compare the answers

Answers from Claude Sonnet 5.5 and GPT-6.1 Sol · try 1 of 1

Same request, same model, once with the skill and once without. Read both and judge for yourself.

“Write the postmortem for yesterday's incident from these notes.”

Source material given with the request

Parcelly incident notes, 2026-10-14 (all times UTC). Pasted from #inc-label-api. 13:58 Deploy 2026.10.14-3 runs migration 0142: CREATE INDEX on shipments(carrier_id, created_at). shipments has ~48M rows. Index built without CONCURRENTLY, so writes to shipments block. 14:03 First support ticket (Bramble Goods): "label creation hanging". 14:06 11 more tickets. 14:09 Support posts in #eng asking if anything is down. 14:12 PagerDuty: label-api p99 latency > 10s for 10 min. On-call Dana Okafor paged. 14:15 Dana acks, starts looking at label-api pods (look healthy). 14:24 Dana finds ~3,900 queries waiting on a lock in pg_stat_activity, traces it to the index build. 14:27 Decision: cancel the index build. 14:29 Build cancelled. Writes resume, backlog starts draining. 14:41 p99 back to normal. Incident closed. Impact: ~6,200 label requests failed or timed out; 214 merchants affected; 57 support tickets. Client retries created 38 duplicate labels, voided by hand ($412 in carrier fees refunded). No data lost. From the debrief: - Ravi Menon wrote migration 0142. Someone wrote "Ravi should have known not to run this during
Show the rest (8 more lines)
business hours, rookie mistake." - Migrations run automatically on deploy. No lock_timeout is set for migrations. - Our migration linter checks naming only; it does not flag CREATE INDEX without CONCURRENTLY. - The latency alert needs 10 minutes over threshold before it fires. - No runbook for lock contention; Dana spent 9 minutes checking pods first. - Dana offered to add a lock_timeout to the migration runner. The Platform team (lead: Sofia Reyes) owns the linter.
with the skill
# Postmortem: Label API outage from a blocking index migration **Date of incident**: 2026-10-14 (all times UTC) **Authors**: TBD **Status**: Draft **Severity**: SEV2 (proposed, to be confirmed) **Duration**: 43 minutes (13:58 to 14:41). Writes to `shipments` were blocked for 31 minutes (13:58 to 14:29). ## Executive Summary On 2026-10-14, deploy `2026.10.14-3` automatically ran migration 0142, which created an index on `shipments(carrier_id, created_at)` without `CONCURRENTLY`. `shipments` has about 48M rows, so the build held a lock that blocked all writes to the table. Label creation hung for 31 minutes. The on-call engineer traced the problem to the index build and cancelled it, after which writes resumed and the backlog drained. Several gaps let this reach production and lengthened the incident. Nothing in the pipeline flags or limits a blocking migration, the latency alert needs 10 minutes to fire, and there is no runbook for lock contention. **Impact** - ~6,200 label requests failed or timed out - 214 merchants affected - 57 support tickets - 38 duplicate labels created by client retries, voided by hand ($412 in carrier fees refunded) - No data lost
Show the rest (104 more lines)
## Timeline | Time | Event | | ---- | ----- | | 13:58 | Deploy 2026.10.14-3 runs migration 0142 (`CREATE INDEX` on `shipments`, not concurrent). Writes to `shipments` begin blocking. | | 14:03 | First support ticket (Bramble Goods): "label creation hanging" | | 14:06 | 11 more tickets | | 14:09 | Support asks in #eng whether anything is down | | 14:12 | PagerDuty fires: label-api p99 latency > 10s for 10 min. Dana Okafor paged. | | 14:15 | Dana acks and starts checking label-api pods, which look healthy | | 14:24 | Dana finds ~3,900 queries waiting on a lock in `pg_stat_activity` and traces it to the index build | | 14:27 | Decision to cancel the index build | | 14:29 | Build cancelled. Writes resume and the backlog starts draining. | | 14:41 | p99 back to normal. Incident closed. | ## Root Cause Analysis ### What happened Migration 0142 built an index on a 48M-row table without `CONCURRENTLY`. A non-concurrent build blocks writes for its whole duration. The migration ran automatically as part of the deploy, so label creation queued behind the lock until the build was cancelled. ### Why it happened 1. **Proximate cause**: a non-concurrent `CREATE INDEX` ran against a large, write-heavy table. 2. **Contributing factors**: - **No automated check caught it.** The migration linter checks naming only and does not flag `CREATE INDEX` without `CONCURRENTLY`. - **No limit on lock waits.** The migration runner sets no `lock_timeout`, so the migration could block production writes indefinitely. - **Migrations run automatically on deploy** with no gate for risky operations on large tables. - **Slow detection.** The latency alert requires 10 minutes over threshold, so customers (12 tickets by 14:06) noticed before the page at 14:12. Support's question in #eng at 14:09 had no path to a page. - **Slow diagnosis.** There is no runbook for lock contention. Dana spent about 9 minutes (14:15 to 14:24) checking healthy pods before finding the lock. - **Retries amplified the damage.** Client retries produced 38 duplicate labels with real carrier fees. This suggests label creation is not idempotent, which should be confirmed. ### 5 Whys 1. **Why did label creation fail?** Writes to `shipments` were blocked by a lock. 2. **Why was there a lock?** A non-concurrent index build on a 48M-row table was running. 3. **Why did that build run in production?** The migration was written that way, and nothing in review, linting, or the runner flagged or limited it. 4. **Why did nothing flag it?** The linter covers naming only, and the runner has no `lock_timeout` or risky-operation gate. 5. **Why were those safeguards missing?** Migration safety has not been treated as a platform-level concern, so a single migration's correctness depended on the author knowing this Postgres behavior. ### A note on the "rookie mistake" comment The debrief included a comment that the migration author should have known better. This postmortem does not treat the author's actions as a cause. The migration was a reasonable-looking change that passed every check we have. Postgres's locking behavior is easy to miss, and our process has no step that would catch it. If the system relies on every author remembering this, it will fail again with someone else. The fixes below add that safety net. ## Detection **What worked** - The latency alert fired and paged on-call, and Dana acknowledged within 3 minutes. **What didn't** - The 10-minute alert window meant a 14-minute gap between the deploy and the page. - Support saw the problem first (14:03) but had no path to a page. - No alert correlates a deploy with a migration that is still running. ## Response **What worked** - Once the lock was found, the decision to cancel took 3 minutes (14:24 to 14:27). - Cancelling the build resumed writes immediately, and recovery took about 12 more minutes. **What could be improved** - Diagnosis took 9 minutes because the first instinct (check the pods) was reasonable but wrong, and nothing pointed toward the database. - No runbook covers lock contention or `pg_stat_activity` triage. - Duplicate labels had to be voided by hand. ## Lessons Learned **What went well** - On-call acknowledged quickly and found the root cause with the tools at hand. - Cancelling the build was a clean, fast mitigation. - No data was lost. **What went wrong** - A blocking migration reached production unchecked. - Customers detected the issue before our alerting did. - Retries created duplicate labels and refunded fees. **Where we got lucky** - The build could be cancelled without damaging the table. - The backlog drained within about 12 minutes of the cancel. ## Action Items Owners are filled in only where the notes assign them. Tickets and due dates still need to be created. The proposed priorities are open for discussion in the review meeting. | Priority | Action | Owner | Due | Ticket | | -------- | ------ | ----- | --- | ------ | | P0 | Add a `lock_timeout` to the migration runner so migrations fail fast instead of blocking writes | Dana Okafor (offered) | TBD | TBD | | P0 | Extend the migration linter to flag `CREATE INDEX` without `CONCURRENTLY` (and other blocking operations) and fail the deploy | Platform team (Sofia Reyes) | TBD | TBD | | P1 | Write a runbook for lock contention (`pg_stat_activity` triage, how to cancel safely) and link it from the label-api alerts | TBD | TBD | TBD | | P1 | Shorten alert detection for label-api (for example, a faster-burn latency or error-rate alert) | TBD | TBD | TBD | | P1 | Investigate making label creation idempotent, or otherwise preventing duplicates on retry | TBD | TBD | TBD | | P2 | Add a path from a surge in support tickets to a page, or at least to on-call | TBD | TBD | TBD | | P2 | Consider a gate or separate step for migrations on large tables rather than auto-running on every deploy | TBD | TBD | TBD | ## Open Questions - What is the current state of migration 0142? Was the index eventually created concurrently, and was the migration marked as applied or rolled back? - Is label creation currently idempotent? Do clients send an idempotency key? - Was the deploy rolled back, or did the rest of `2026.10.14-3` stay live? - What severity does this meet under our definitions? ## Related Incidents None identified in the notes. Check for earlier migration-related lock incidents. I left out ticket numbers, due dates, and unassigned owners rather than invent them. Fill those in at the review meeting.
without
# Postmortem: Label API outage caused by blocking index migration **Date:** 2026-10-14 (all times UTC) **Duration:** 13:58 to 14:41 (43 min). Writes to `shipments` were blocked for about 31 min (13:58 to 14:29). **Severity:** Customer-facing outage of label creation **Status:** Resolved ## Summary Deploy 2026.10.14-3 ran migration 0142 automatically, which created an index on `shipments(carrier_id, created_at)` without `CONCURRENTLY`. On a table of about 48M rows, the build held a lock that blocked all writes to `shipments`, so label creation hung and timed out. On-call cancelled the build at 14:29 and service recovered by 14:41. No data was lost. ## Impact - ~6,200 label requests failed or timed out - 214 merchants affected - 57 support tickets - 38 duplicate labels created by client retries, all voided by hand ($412 in carrier fees refunded) ## Timeline
Show the rest (49 more lines)
| Time | Event | |---|---| | 13:58 | Deploy 2026.10.14-3 runs migration 0142 (`CREATE INDEX` without `CONCURRENTLY`). Writes to `shipments` begin blocking. | | 14:03 | First support ticket (Bramble Goods): "label creation hanging" | | 14:06 | 11 more tickets | | 14:09 | Support asks in #eng whether anything is down | | 14:12 | PagerDuty fires: label-api p99 > 10s for 10 min. Dana Okafor paged. | | 14:15 | Dana acks and checks label-api pods, which look healthy | | 14:24 | Dana finds ~3,900 queries waiting on a lock in `pg_stat_activity` and traces it to the index build | | 14:27 | Decision made to cancel the index build | | 14:29 | Build cancelled. Writes resume and the backlog starts draining. | | 14:41 | p99 back to normal. Incident closed. | ## Root cause A migration that takes a write-blocking lock for a long time on a very large table was able to reach production and run automatically. `CREATE INDEX` without `CONCURRENTLY` blocks writes for the duration of the build, and on 48M rows that duration was far longer than the API could tolerate. ## Contributing factors 1. **No guardrail on migration content.** The migration linter checks naming only and does not flag `CREATE INDEX` without `CONCURRENTLY`. 2. **No lock timeout.** Migrations run with no `lock_timeout`, so the migration held its lock and blocked writes until someone intervened, instead of failing fast. 3. **Migrations run automatically on deploy.** There was no review point or staging check for the risk of a long-running migration against production-sized data. 4. **Slow detection.** The latency alert requires 10 minutes over threshold, so the page came 14 minutes after the deploy, and customers and support noticed before we did. Support's question in #eng at 14:09 did not lead to an engineering response. 5. **No runbook for lock contention.** Dana spent 9 minutes (14:15 to 14:24) checking healthy pods before looking at database locks. The signal was visible in `pg_stat_activity` once checked. 6. **Client retries amplified the damage.** Retries of timed-out requests created 38 duplicate labels that cost money and needed manual cleanup. ## What went well - Once Dana looked at the database, she found the cause in under 10 minutes and the decision to cancel took 3 minutes. - Cancelling the build recovered the system cleanly, with a fast backlog drain and no data loss. - Support escalated quickly, with a ticket within 5 minutes of the problem starting. ## Action items | # | Action | Owner | Addresses | |---|---|---|---| | 1 | Set `lock_timeout` (and consider `statement_timeout`) in the migration runner so blocking migrations fail fast | Dana Okafor | Factor 2 | | 2 | Add a linter rule that flags `CREATE INDEX` without `CONCURRENTLY` (and other known blocking DDL) and fails the build | Platform (Sofia Reyes) | Factor 1 | | 3 | Write a runbook for DB lock contention, starting with checking `pg_stat_activity` for waiting queries, and link it from the label-api alert | Owner TBD | Factor 5 | | 4 | Lower the alert window or add a faster signal (for example, lock waits or error rate) so that detection takes minutes, not 10+ | Owner TBD | Factor 4 | | 5 | Define a path for large-table migrations: review requirement or a separate non-automatic step for tables above a size threshold | Owner TBD | Factor 3 | | 6 | Add a route so support reports of widespread issues page on-call | Owner TBD | Factor 4 | | 7 | Investigate idempotency keys for label creation so client retries can't create duplicates | Owner TBD | Factor 6 | Items marked "Owner TBD" need an owner and due date assigned at the follow-up review. ## Note on blame Ravi Menon authored migration 0142, and the debrief included a comment that this was a "rookie mistake" that he "should have known" to avoid. We don't treat it that way. Any engineer can write this migration, and nothing in our process stopped it: no linter rule, no lock timeout, no review step, and automatic execution on deploy. The fix is to make this mistake hard to ship, not to rely on individuals remembering it.

Answers are shown exactly as the model wrote them, from the first try, and nothing is picked by hand.

The models

2 models

Claude Sonnet 5.5

made by Anthropic

claude-sonnet-5-5 · revision not reported by the provider

1 try per case · tested 3 Nov 2026

GPT-6.1 Sol

made by OpenAI

gpt-6.1-sol · revision not reported by the provider

1 try per case · tested 3 Nov 2026

The tasks

written by Carte Blanche
blameless-rewrite-tls

“My manager says this postmortem draft is too blamey. Can you rewrite the root cause and action items?”

fix-weak-action-items

“Our postmortem's action items feel weak. Can you rewrite them?”

full-postmortem-from-notes

“Write the postmortem for yesterday's incident from these notes.”

quick-sev3-writeup

“Can you write a short postmortem for this small incident? We don't need the full template, just something for the incident log.”

root-cause-past-disk-full

“Our draft root cause just says 'disk filled up on search-node-3'. Is that enough? Help me write a better root cause section.”

Install

Download the .zip and unzip it. Put its files, in a folder named postmortem-writing, here:

Claude Code~/.claude/skills/Every project. For one project only, use .claude/skills/ inside it.
Codex~/.agents/skills/Every project. For one repository only, use .agents/skills/ inside it.
Cursor~/.cursor/skills/Cursor also reads the Claude Code and Codex folders above.

Then start a new session and ask for what the skill does. Other agents that read SKILL.md have their own skills folder; check their docs.

Coming after launch: get skills from inside your agent, no download needed.