Skip to content

fix(research): return 503, not 500, when the events provider is at capacity - #828

Merged
sweetmantech merged 1 commit into
mainfrom
fix/events-capacity-503
Aug 11, 2026
Merged

fix(research): return 503, not 500, when the events provider is at capacity#828
sweetmantech merged 1 commit into
mainfrom
fix/events-capacity-503

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

POST /api/research/events returns a 500 carrying Apify's own error text when the shared provider quota is saturated. Found by running a real 67-artist customer roster through the live endpoint: two artists were silently lost.

Tracking issue: recoupable/chat#1954

What is actually happening

Social scrapes and event lookups run on one shared Apify account. Measured directly on 2026-08-11:

maxActorMemoryGbytes: 64
maxConcurrentActorJobs: 32

The Bandsintown actor takes roughly 4GB per run, so about 16 can be in flight at once across every caller. Past that, Apify refuses to launch:

By launching this job you will exceed the memory limit of 65536MB

Reproduced cleanly:

Concurrency Failures
16 0 / 16
20 4 / 20

The failing artists are not special. Called individually they all return 200 in under 3s. It is purely a function of how many runs are in flight.

Why the current behaviour is wrong twice over

  1. A 500 tells callers their request is broken and to give up. The request is fine; a retry moments later succeeds. This is a textbook 503.
  2. It echoes the provider's message to API consumers, leaking which vendor we use and our account's memory ceiling.

Both matter in practice. The agent that consumes this endpoint saw HTTP 500 after retry, counted the artists as failures, and moved on. Neither had upcoming shows so nothing was lost that day, but a failed lookup and an artist with no shows are indistinguishable to the caller, which is the silent-wrong-answer shape this endpoint exists to avoid.

The change

isApifyCapacityError recognises the account-level rejections (memory limit, concurrent-run cap, rate limit) and the handler maps them to:

{ "status": "error", "error": "Events provider is at capacity. Retry this request after a short delay." }

A failed actor run stays a 500. That is a genuine upstream fault, and conflating "the provider is busy" with "the run broke" would remove the caller's ability to decide whether retrying is worth it. There is a test asserting the two stay distinct.

Verification

Check Result
vitest run lib/research lib/apify lib/artists 615 passed / 128 files (607 before, +8)
New tests 8 (7 detector, 1 handler)
eslint on touched files clean

Both units were written RED → GREEN: the detector failed on missing module, the handler test failed with expected 500 to be 503.

Two follow-ups this does not do

  • Retry-After header. Genuinely useful for a capacity signal, but the shared errorResponse helper takes no headers and widening it touches every endpoint in the repo. Worth doing deliberately rather than as a side effect here.
  • Docs. The spec documents 200/400/401/402/404 and already omitted 500; this adds 503. The contract gap is now wider and should be closed in a docs PR, deferred per direction.

The consuming task prompt has been updated separately to cap its own concurrency at 5, run its scrape pass and its events pass sequentially rather than in parallel, and back off exponentially on this response instead of retrying once.

🤖 Generated with Claude Code


Summary by cubic

Return 503 instead of 500 from POST /api/research/events when the provider is at capacity, so clients know to retry and we don’t leak provider internals.

  • Bug Fixes
    • Detect capacity rejections (memory limit, concurrent runs, rate limit) with isApifyCapacityError and map them to 503 with a clean message.
    • Keep failed actor runs as 500 to distinguish real upstream faults from capacity.

Written for commit 8322c28. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of service capacity errors.
    • Capacity-related failures now return a clear temporary-unavailability response, advising callers to retry instead of showing a generic server error.

…pacity

Social scrapes and event lookups share one Apify account. Measured 2026-08-11:
64GB max actor memory, 32 max concurrent jobs, and the Bandsintown actor takes
about 4GB per run, so roughly 16 can be in flight across every caller. Past
that Apify refuses to launch with "By launching this job you will exceed the
memory limit of 65536MB".

The handler's catch-all turned that into a 500 carrying Apify's own text. Two
problems: a 500 tells callers their request is broken and to give up, when the
request is fine and a retry would succeed; and it leaks provider internals to
API consumers.

Adds isApifyCapacityError and maps capacity rejections to 503 with a clean
message. A failed actor run stays a 500, since that is a genuine upstream
fault and callers should treat the two differently.

Observed in production: a 67-artist roster sweep lost 2 artists to this.
Reproduced at 4 failures in 20 concurrent requests, 0 in 16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Aug 11, 2026 5:40pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an Apify capacity-error classifier and uses it in the research events handler. Detected capacity errors now return a sanitized 503 response with retry guidance instead of a generic 500 response.

Changes

Apify capacity handling

Layer / File(s) Summary
Capacity error classifier
lib/apify/isApifyCapacityError.ts
Adds isApifyCapacityError(error: unknown): boolean. The predicate checks Error and string messages against known Apify memory, concurrency, and rate-limit signatures.
Research handler response
lib/research/postResearchEventsHandler.ts
Uses the classifier in the error path. Capacity errors return a sanitized 503 response with retry guidance. Other errors retain the generic handling path.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • recoupable/api#826: Extends the same research events handler with Apify capacity-error handling.

Poem

Capacity fills the Apify gate,
The classifier marks its state.
A 503 says, “Try again,”
While hidden details stay within.
Clean errors guide the waiting queue.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning The new detector is focused, but the changed postResearchEventsHandler remains a 61-line function with validation, lookup, fetching, billing, and response responsibilities, exceeding the 20-line SR... Refactor postResearchEventsHandler into focused helpers for validation/artist resolution, event fetching, credit deduction, and error-response mapping.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/events-capacity-503

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sweetmantech
sweetmantech merged commit 0d57c99 into main Aug 11, 2026
5 of 6 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/research/postResearchEventsHandler.ts (1)

78-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the handler size.

postResearchEventsHandler spans Lines 34-94. Extract the provider call and error mapping into focused private helpers. This keeps request orchestration separate from provider failure classification and makes the 503/500 contract easier to test.

As per coding guidelines, “Flag functions longer than 20 lines” and “Keep functions small and focused.” As per path instructions, “Keep functions under 50 lines.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/research/postResearchEventsHandler.ts` around lines 78 - 88, Refactor
postResearchEventsHandler so its provider invocation and error-to-response
mapping move into focused private helper functions. Keep the handler responsible
only for request orchestration, preserve the existing isApifyCapacityError 503
response and other 500 behavior, and ensure each function remains within the
project’s size limits.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/research/postResearchEventsHandler.ts`:
- Around line 78-88: Scope the isApifyCapacityError check in the post-research
events handler to errors originating from fetchBandsintownEvents, preventing
validation or artist lookup failures with matching text from returning 503.
Preserve the existing 500 behavior for those lookup errors and add a regression
test asserting HTTP 500.

---

Nitpick comments:
In `@lib/research/postResearchEventsHandler.ts`:
- Around line 78-88: Refactor postResearchEventsHandler so its provider
invocation and error-to-response mapping move into focused private helper
functions. Keep the handler responsible only for request orchestration, preserve
the existing isApifyCapacityError 503 response and other 500 behavior, and
ensure each function remains within the project’s size limits.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 72591d02-5e3d-43c8-8d27-5fe47bb8b9f2

📥 Commits

Reviewing files that changed from the base of the PR and between d55370d and 8322c28.

⛔ Files ignored due to path filters (2)
  • lib/apify/__tests__/isApifyCapacityError.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/research/__tests__/postResearchEventsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/apify/isApifyCapacityError.ts
  • lib/research/postResearchEventsHandler.ts

Comment on lines +78 to +88
// A saturated provider quota is a capacity condition, not a server fault:
// the caller's request is fine and retrying later will work. Reporting it
// as a 500 tells callers to give up, and echoing the provider's own text
// leaks our infrastructure to them.
if (isApifyCapacityError(error)) {
return errorResponse(
"Events provider is at capacity. Retry this request after a short delay.",
503,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope capacity detection to the Apify fetch.

This outer catch also handles validatePostResearchEventsRequest, getArtists, and getArtistBandsintownId. If one of those operations throws an error containing rate limit exceeded or another configured signature, the handler returns a misleading 503 stating that the Events provider is at capacity.

Apply isApifyCapacityError only around fetchBandsintownEvents, or require an Apify-specific error source before returning 503. Add a regression test for a lookup error with the same message and expect HTTP 500.

Suggested scoping
-    const events = await fetchBandsintownEvents({
-      bandsintownId,
-      ...(validated.date && { date: validated.date }),
-    });
+    let events: Awaited<ReturnType<typeof fetchBandsintownEvents>>;
+    try {
+      events = await fetchBandsintownEvents({
+        bandsintownId,
+        ...(validated.date && { date: validated.date }),
+      });
+    } catch (error) {
+      if (isApifyCapacityError(error)) {
+        return errorResponse(
+          "Events provider is at capacity. Retry this request after a short delay.",
+          503,
+        );
+      }
+      throw error;
+    }
@@
-    if (isApifyCapacityError(error)) {
-      return errorResponse(
-        "Events provider is at capacity. Retry this request after a short delay.",
-        503,
-      );
-    }
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A saturated provider quota is a capacity condition, not a server fault:
// the caller's request is fine and retrying later will work. Reporting it
// as a 500 tells callers to give up, and echoing the provider's own text
// leaks our infrastructure to them.
if (isApifyCapacityError(error)) {
return errorResponse(
"Events provider is at capacity. Retry this request after a short delay.",
503,
);
}
let events: Awaited<ReturnType<typeof fetchBandsintownEvents>>;
try {
events = await fetchBandsintownEvents({
bandsintownId,
...(validated.date && { date: validated.date }),
});
} catch (error) {
if (isApifyCapacityError(error)) {
return errorResponse(
"Events provider is at capacity. Retry this request after a short delay.",
503,
);
}
throw error;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/research/postResearchEventsHandler.ts` around lines 78 - 88, Scope the
isApifyCapacityError check in the post-research events handler to errors
originating from fetchBandsintownEvents, preventing validation or artist lookup
failures with matching text from returning 503. Preserve the existing 500
behavior for those lookup errors and add a regression test asserting HTTP 500.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant