Resolving PA Pend Race Conditions
When two claims for the same member and drug hit the adjudicator within the same few milliseconds — a pharmacy resubmit racing the original, or a point-of-sale claim racing a prescriber-portal PA request — both can observe “no open prior-authorization case” and both can create one. The decision this page settles is exactly how to stop that: how to guarantee that concurrent threads pending the same (member_token, GPI) converge on a single PA case, with no duplicate PENDING rows and no lost update, without serializing the whole adjudication path behind a lock. The answer is a deterministic case identity plus an atomic upsert guarded by a database unique constraint, and the sections below compare that against the alternatives and implement it. This is the concurrency hardening the Prior Authorization Routing Automation state machine assumes.
The Concurrency Decision
The naive implementation is check-then-create: read whether an open case exists for the member and drug, and if not, insert one. Under concurrency that read-then-write is a classic time-of-check-to-time-of-use gap — both threads read “none,” both write, and the store now holds two cases that will accrue two reviews and can later reach two conflicting decisions. Four mechanisms can close the gap; they differ sharply in latency, blast radius, and how they behave under at-least-once queue redelivery.
| Mechanism | How it prevents the duplicate | Latency cost | Failure behavior | Fit for PA pend |
|---|---|---|---|---|
| Pessimistic row/advisory lock | Serialize writers on a lock keyed by (member_token, GPI) |
High — holds a lock across the decision, contends under load | Lock timeouts, deadlocks if lock order varies | Poor on the hot path; blocks unrelated claims |
| Optimistic concurrency (version check) | Read version, write conditional on unchanged version, retry on conflict | Low when uncontended; retry storms when hot | Livelock if many writers retry the same key | Good for updates to an existing case, not first-create |
| Idempotency-key dedup (cache) | First writer records the key in Redis/DynamoDB; later writers see it | Very low | Lost dedup if the cache evicts the key mid-race | Good as a fast pre-filter, not a system of record |
| DB unique constraint + atomic upsert | INSERT ... ON CONFLICT on a UNIQUE(case_id) — the DB decides the winner |
Low, single round trip | Deterministic; loser gets the existing row, never an error surfaced to the caller | Best — the authoritative guarantee |
The production answer combines two of these. The unique constraint on a deterministically derived case_id is the system of record guarantee — it cannot be raced because uniqueness is enforced inside the single-writer commit path of the database. An idempotency-key check in the ephemeral claim-state cache sits in front of it as a cheap pre-filter so most duplicates never reach the database at all; the trade-offs of Redis versus DynamoDB for that cache are worked through in Redis vs DynamoDB for Ephemeral Claim State. Optimistic version checks then govern subsequent transitions on the case, which is a different problem from first-create.
Figure: Both threads derive the identical case_id; the UNIQUE(case_id) constraint lets the first writer create the case and forces the second to attach to it — collapsing the race to one PA case.
Step-by-Step Implementation
The implementation has three moves: derive a collision-proof identity, dedup cheaply in the cache, then commit through an atomic upsert whose conflict clause returns the existing row.
1. Derive a deterministic case_id. The identity must be a pure function of the fields that define “the same PA question,” so both threads compute the same value independently. Use member_token (never 302-C2 Cardholder ID), the GPI (never the raw 407-D7 NDC), and a coarse request_window (for example the ISO date) so rapid resubmits fall in one window.
import hashlib
def derive_case_id(member_token: str, gpi: str, request_window: str) -> str:
# PHI GUARDRAIL: member_token is already opaque; 302-C2 never appears here.
# Deterministic so concurrent threads independently produce the same id.
raw = f"{member_token}|{gpi}|{request_window}".encode("utf-8")
return hashlib.sha256(raw).hexdigest()[:32]2. Pre-filter with an idempotency key in ephemeral state. A SET key value NX in the claim-state cache lets the first thread claim the case cheaply so most duplicates never touch the database. Treat a cache miss or eviction as “not sure” and fall through to the authoritative upsert — never as proof the case is new.
3. Commit through an atomic upsert. The database, not application code, decides the winner. INSERT ... ON CONFLICT (case_id) DO UPDATE with a RETURNING clause hands back the row whether it was just created or already existed, and a version guard makes any follow-on transition conditional.
import json
import logging
from dataclasses import dataclass
logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("pa_pend")
@dataclass(frozen=True)
class PendResult:
case_id: str
state: str
created: bool # True only for the writer that actually inserted
UPSERT_SQL = """
INSERT INTO pa_cases (case_id, member_token, gpi, state, version, request_window)
VALUES (%(case_id)s, %(member_token)s, %(gpi)s, 'PENDING', 1, %(request_window)s)
ON CONFLICT (case_id) DO UPDATE
SET touched_at = now() -- no-op update so RETURNING yields the row
RETURNING case_id, state, (xmax = 0) AS created;
"""
def pend_case(cur, member_token: str, gpi: str, request_window: str) -> PendResult:
"""Idempotently open (or attach to) the PA case for this member+GPI+window."""
case_id = derive_case_id(member_token, gpi, request_window)
cur.execute(UPSERT_SQL, {
"case_id": case_id,
"member_token": member_token, # opaque, non-PHI
"gpi": gpi, # therapeutic class, not raw 407-D7 NDC
"request_window": request_window,
})
row = cur.fetchone()
result = PendResult(case_id=row[0], state=row[1], created=bool(row[2]))
# Audit: tokens + outcome only, never 302-C2 / 310-CA / raw claim bytes.
logger.info(json.dumps({
"event": "pa_pend",
"case_id": result.case_id,
"member_token": member_token,
"gpi": gpi,
"created": result.created,
"reject_code": "75", # 511-FB: Prior Authorization Required
}))
return resultThe xmax = 0 trick reads PostgreSQL’s system column to tell “I inserted this” from “I hit the conflict,” so the winner starts the review workflow exactly once while the loser silently attaches. No lock is held across the decision; the only serialization point is the database’s own uniqueness check, which is single-writer by construction.
Verification and Testing Pattern
The property to pin is: N concurrent pend_case calls for identical inputs create exactly one case, and exactly one caller sees created=True. Drive it with a thread pool against a real transactional store (a unique constraint is the thing under test, so an in-memory dict would prove nothing).
import pytest
from concurrent.futures import ThreadPoolExecutor
def test_concurrent_pend_creates_one_case(db_pool):
member_token, gpi, window = "MTOK_9f2a", "27100010100320", "2026-07-17"
def worker():
with db_pool.connection() as conn, conn.cursor() as cur:
r = pend_case(cur, member_token, gpi, window)
conn.commit()
return r
with ThreadPoolExecutor(max_workers=16) as pool:
results = [f.result() for f in [pool.submit(worker) for _ in range(16)]]
case_ids = {r.case_id for r in results}
assert len(case_ids) == 1 # one identity, no rivals
assert sum(1 for r in results if r.created) == 1 # exactly one true creator
assert all(r.state == "PENDING" for r in results)
def test_case_id_is_deterministic():
a = derive_case_id("MTOK_9f2a", "27100010100320", "2026-07-17")
b = derive_case_id("MTOK_9f2a", "27100010100320", "2026-07-17")
assert a == b # independent threads agreeThe first assertion is the load-bearing one: if the unique constraint or the identity derivation is wrong, more than one distinct case_id appears or more than one caller claims created, and the test fails deterministically under the thread pool.
Gotchas and PHI Guardrails
- Windowing granularity. Too coarse a
request_window(a whole month) merges legitimately distinct PA requests; too fine (a timestamp) defeats dedup because two racing claims land in different windows. A calendar date is the usual sweet spot; tune it to the plan’s resubmission behavior. - Cache eviction is not a decision. The idempotency-key pre-filter is an optimization only. If the cache evicts the key mid-race, the database unique constraint must still hold — never treat a cache miss as authority to create.
- Lost updates on follow-on transitions. First-create is solved by uniqueness; a later
PENDING → APPROVEDstill needs an optimisticversionguard so a stale writer cannot clobber a newer decision. Retry on version conflict, do not lock. - Deadlock from pessimistic locking. If you fall back to row locks, always acquire in a fixed key order; inconsistent lock ordering across
(member, GPI)pairs deadlocks under load. The atomic-upsert path avoids this entirely. - Fail closed on store outage. If the upsert cannot commit, the claim re-asserts reject
75and degrades through Fallback Routing Logic Design; never default to creating an unpersisted “approved” case in memory. - PHI in logs. Log
case_id,member_token,gpi, and thecreatedflag only. The raw302-C2Cardholder ID and310-CAPatient Name were stripped upstream; a debug line echoing the claim to diagnose a race is a reportable breach.
Related
- Prior Authorization Routing Automation — the PA state machine whose deterministic
case_idthis page enforces under concurrency. - Automating PA Override Approvals — the sibling decision that runs once a single case has been established.
- Redis vs DynamoDB for Ephemeral Claim State — choosing the cache that backs the idempotency-key pre-filter in front of the unique constraint.
- Fallback Routing Logic Design — how the pend path fails closed with reject
75when the case store is unreachable.
← Back to Prior Authorization Routing Automation