Skip to content

fix: stabilize OG image generation under bot crawler load - #2089

Open
gaspergrom wants to merge 3 commits into
mainfrom
feat/IN-1200
Open

fix: stabilize OG image generation under bot crawler load#2089
gaspergrom wants to merge 3 commits into
mainfrom
feat/IN-1200

Conversation

@gaspergrom

Copy link
Copy Markdown
Collaborator

Summary

Implements multi-layered approach to fix OG image 500 errors under concurrent bot crawler load (IN-1200):

  • Timeout handling (8s): Prevents slow Satori/Resvg rendering from blocking
  • Bot rate limiting (5 req/s per bot): Targets bingbot, PetalBot, Amzn-SearchBot
  • Response caching (24h): Reduces rendering load from repeated requests
  • Collection validation: Mirrors project middleware, prevents invalid data to renderer
  • Enhanced error handling: Better logging and timeout detection

Files Changed

  • frontend/server/middleware/og-image-timeout.ts (new)
  • frontend/server/middleware/og-image-bot-limiter.ts (new)
  • frontend/server/middleware/og-image-cache-headers.ts (new)
  • frontend/server/middleware/og-image-collection.ts (new)
  • frontend/server/plugins/og-image-fallback.ts (enhanced)
  • frontend/setup/og-image.ts (cache config added)

How to Test

  1. Verify timeout kicks in for slow renders
  2. Test rate limiting with bot user agents
  3. Confirm cache headers are set on responses
  4. Validate collection endpoint redirects deleted collections to fallback
  5. Load test with concurrent bot-like requests

Out of Scope

  • robots.txt modifications
  • CDN-level edge caching
  • Kubernetes/ingress bot throttling
  • Rendering service resource tuning

These infrastructure changes are being tracked separately.

… under bot load IN-1200

Implement multi-layered approach to stabilize OG image generation under concurrent bot crawler requests:

1. Timeout handling (8s): Prevents slow Satori/Resvg rendering from blocking
2. Bot rate limiting (5 req/s per bot): Targets bingbot, PetalBot, Amzn-SearchBot, et al.
3. Response caching (24h): Reduces rendering load from repeated requests
4. Collection validation: Prevents 500s on non-existent collections (mirrors project middleware)
5. Improved fallback: Enhanced error logging and timeout detection

These changes address the root cause (rendering timeouts) while also reducing load via caching and rate limits. Cache control headers enable CDN/browser caching of generated images.

Suggested investigation items from ticket remain out-of-scope:
- robots.txt bot exclusion (infrastructure change)
- CDN-level edge caching (infra configuration)
- Rendering service resource limits (infrastructure config)

This fix focuses on application-level stabilization that can be deployed immediately.

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Copilot AI balanced review requested due to automatic review settings August 14, 2026 23:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds safeguards intended to stabilize OG image generation under crawler load.

Changes:

  • Adds timeout, bot throttling, and cache handling.
  • Validates collection OG requests.
  • Expands fallback error logging.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
frontend/setup/og-image.ts Configures OG cache duration.
frontend/server/plugins/og-image-fallback.ts Enhances fallback logging.
frontend/server/middleware/og-image-timeout.ts Introduces render timeout handling.
frontend/server/middleware/og-image-collection.ts Validates collection slugs.
frontend/server/middleware/og-image-cache-headers.ts Adds response cache headers.
frontend/server/middleware/og-image-bot-limiter.ts Rate-limits known crawlers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread frontend/setup/og-image.ts Outdated
Comment on lines +37 to +39
cache: {
maxAge: 60 * 60 * 24, // 24 hours in seconds
},
Comment on lines +22 to +27
const timeoutId = setTimeout(() => {
timeoutController.abort();
}, OG_IMAGE_RENDER_TIMEOUT_MS);

// Store timeout controller on the event for potential use in handlers
(event.node as any).__ogImageTimeoutAbort = timeoutController;
error.message.includes('AbortError'));

const errorType = isTimeout ? 'TIMEOUT' : 'ERROR';
const severity = isTimeout ? 'WARN' : 'ERROR';
setHeader(event, 'Vary', 'User-Agent');

// Log cache headers being applied
onBeforeSendResponse(async (context) => {
Comment on lines +29 to +34
const res = await fetchFromTinybird<Record<string, unknown>[]>(
'/v0/pipes/collections_list.json',
{
slug,
details: true,
}
Comment on lines +20 to +24
setHeader(
event,
'Cache-Control',
`public, max-age=${CACHE_MAX_AGE_SECONDS}, stale-while-revalidate=${STALE_WHILE_REVALIDATE}`
);
return null;
}

function isRateLimited(botId: string): boolean {
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
…ng IN-1200

- fix nonexistent onBeforeSendResponse hook in og-image-cache-headers.ts, which
  threw on every OG image request and silently fell back to the static image;
  replace with a writeHead patch that sets Cache-Control by final status code
- fix og-image.ts cache config using the wrong nuxt-og-image schema
  (cache.maxAge instead of defaults.cacheMaxAgeSeconds), which was a no-op
- fix og-image-timeout.ts only logging on timeout instead of actually sending
  the static fallback response
- add periodic eviction of expired entries in og-image-bot-limiter.ts's
  request-tracking map
- add test coverage for all four middleware files

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Copilot AI review requested due to automatic review settings August 15, 2026 03:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

frontend/server/middleware/og-image-bot-limiter.ts:70

  • The new limiter's boundary behavior is untested: the suite sends only one request per recognized bot and never verifies that requests 1–5 pass, request 6 returns 429, and a new window resets the quota. Add a state-isolated test for those transitions so off-by-one or reset regressions are caught.
  // Check if we've exceeded the limit
  if (tracked.count >= RATE_LIMIT_CONFIG.requestsPerSecond) {
    return true;

frontend/server/middleware/og-image-collection.ts:30

  • This validator uses Tinybird, but the renderer fetches /api/collection/:slug, whose source of truth is CommunityCollectionRepository.findBySlug (server/api/collection/[slug]/index.ts:35-40). Any collection missing or lagging in Tinybird—including DB-backed community collections—will be falsely redirected even though rendering can succeed. Validate against the same PostgreSQL source of truth, ideally with a lightweight shared existence query.
    const res = await fetchFromTinybird<Record<string, unknown>[]>(
      '/v0/pipes/collections_list.json',

frontend/server/middleware/og-image-timeout.ts:24

  • Nuxt auto-registers server middleware in filename order, so this timer starts only after og-image-collection.ts and og-image-project.ts finish. Tinybird acquisition alone can wait 10 seconds (server/data/tinybird/tinybird.ts:10,149-153), meaning requests can already exceed the advertised 8-second timeout before this line runs. Register the timeout middleware before validation, for example with an ordering prefix.
  const timeoutId = setTimeout(() => {

frontend/server/middleware/og-image-cache-headers.ts:36

  • The rendered image does not vary by user agent, so this header creates a separate CDN/browser cache entry for every crawler UA and reduces the cache hit rate this PR is intended to improve. It can also overwrite an existing Vary value. Remove Vary: User-Agent and update the corresponding test expectation.
      res.setHeader('Vary', 'User-Agent');

frontend/server/plugins/og-image-fallback.ts:26

  • The previous logging passed the error object itself, but this now logs only its message, dropping the stack trace and error metadata needed to diagnose unexpected renderer failures. Preserve the structured error alongside the formatted context.
      console.error(message);

frontend/server/middleware/og-image-bot-limiter.test.ts:50

  • This user agent matches none of BOT_PATTERNS, so the handler exits through the non-bot branch and the test does not exercise an initial tracked bot request. Use a recognized bot that is not reused by the later tests.
    (global.getHeader as any).mockReturnValue('unique-bot-' + Math.random());

import { sendRedirect } from 'h3';
import { fetchFromTinybird } from '~~/server/data/tinybird/tinybird';

const OG_IMAGE_COLLECTION_PREFIX = '/__og-image__/image/collection/';
@gaspergrom
gaspergrom requested a review from epipav August 15, 2026 04:39
@gaspergrom
gaspergrom marked this pull request as ready for review August 15, 2026 04:39
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.

2 participants