Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add env vars to help test Vercel Queue max 24 hour message TTL - #653

Merged
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl
Dec 19, 2025
Merged

Add env vars to help test Vercel Queue max 24 hour message TTL#653
TooTallNate merged 4 commits into
12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messagefrom
12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Dec 18, 2025

Copy link
Copy Markdown
Member

For testing. May or may not end up merging.

@changeset-bot

changeset-botBot commented Dec 18, 2025

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f033448

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production286011297
❌ 💻 Local Development235278270
✅ 📦 Local Production26208270
✅ 🐘 Local Postgres26208270
✅ 🪟 Windows270027
❌ 🌍 Community Worlds109110120
Total118138351254

❌ Failed Tests

💻 Local Development (27 failed)

fastify-stable (26 failed):

  • addTenWorkflow
  • addTenWorkflow
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

nitro-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (11 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (8 failed):

  • addTenWorkflow
  • addTenWorkflow
  • retryAttemptCounterWorkflow
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro2601
✅ example2601
✅ express2601
✅ fastify2601
✅ hono2601
✅ nextjs-turbopack2601
✅ nextjs-webpack2601
✅ nitro2601
✅ nuxt2601
✅ sveltekit2601
✅ vite2601
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
❌ fastify-stable0261
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
❌ nitro-stable2511
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable2601
✅ express-stable2601
✅ fastify-stable2601
✅ hono-stable2601
✅ nextjs-turbopack-stable2700
✅ nextjs-webpack-stable2700
✅ nitro-stable2601
✅ nuxt-stable2601
✅ sveltekit-stable2601
✅ vite-stable2601
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack2700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb2610
✅ redis-dev300
❌ redis2610
✅ starter-dev300
❌ starter1980
✅ turso-dev300
❌ turso2610

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment threadpackages/world-vercel/src/queue.ts Outdated
Comment on lines +23 to +31
const VERCEL_QUEUE_MAX_VISIBILITY = Number(
process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds
);
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires
);

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.

Environment variables are converted to numbers without validation, which could result in NaN values if invalid values are provided, breaking queue timing logic.

View Details
📝 Patch Details
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index 8b6f791..31377c0 100644
--- a/packages/world-vercel/src/queue.ts+++ b/packages/world-vercel/src/queue.ts@@ -20,14 +20,34 @@ const MessageWrapper = z.object({
});
// Queue timing constants - can be overridden via environment variables for testing
-const VERCEL_QUEUE_MAX_VISIBILITY = Number(- process.env.VERCEL_QUEUE_MAX_VISIBILITY ?? 39600 // 11 hours in seconds+const parseEnvNumber = (+ value: string | undefined,+ defaultValue: number+): number => {+ if (value === undefined) {+ return defaultValue;+ }+ const parsed = Number(value);+ if (Number.isNaN(parsed)) {+ console.warn(+ `Invalid number in environment variable: "${value}". Using default: ${defaultValue}`+ );+ return defaultValue;+ }+ return parsed;+};++const VERCEL_QUEUE_MAX_VISIBILITY = parseEnvNumber(+ process.env.VERCEL_QUEUE_MAX_VISIBILITY,+ 39600 // 11 hours in seconds
);
-const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds+const VERCEL_QUEUE_MESSAGE_LIFETIME = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME,+ 86400 // 24 hours in seconds
);
-const MESSAGE_LIFETIME_BUFFER = Number(- process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires+const MESSAGE_LIFETIME_BUFFER = parseEnvNumber(+ process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER,+ 3600 // 1 hour buffer before lifetime expires
);
export function createQueue(config?: APIConfig): Queue {

Analysis

Invalid environment variables cause NaN in queue timing constants, breaking message re-enqueue logic

What fails: Queue message re-enqueue logic in createQueueHandler() silently fails when environment variables VERCEL_QUEUE_MAX_VISIBILITY, VERCEL_QUEUE_MESSAGE_LIFETIME, or VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER are set to non-numeric values.

How to reproduce:

VERCEL_QUEUE_MAX_VISIBILITY="invalid" \
VERCEL_QUEUE_MESSAGE_LIFETIME="not-a-number" \
VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER="abc" \
node app.js

Result: Constants become NaN. When a message approaches its 24-hour lifetime:

  • Line 102-105: Math.min(result.timeoutSeconds, VERCEL_QUEUE_MAX_VISIBILITY) returns NaN (since any operation with NaN returns NaN)
  • Line 112-113: The condition messageAgeAtNextProcessing > VERCEL_QUEUE_MESSAGE_LIFETIME - MESSAGE_LIFETIME_BUFFER evaluates to false (any comparison with NaN returns false), so messages never get re-enqueued even when approaching the lifetime limit

Expected behavior: Invalid environment variables should fall back to safe defaults with a warning. Messages should be re-enqueued when approaching the 24-hour lifetime limit.

Fix: Added parseEnvNumber() helper that validates parsed numbers with Number.isNaN() and falls back to defaults, warning when invalid values are provided.

@TooTallNate
TooTallNate marked this pull request as ready for review December 19, 2025 18:51
CopilotAI review requested due to automatic review settings December 19, 2025 18:51
@TooTallNate
TooTallNate merged commit 7bbba1c into 12-18-handle_vercel_queue_24_hour_message_ttl_by_re-enqueueing_messageDec 19, 2025
68 of 71 checks passed
@TooTallNate
TooTallNate deleted the 12-18-add_env_vars_to_help_test_vercel_queue_max_24_hour_message_ttl branch December 19, 2025 18:51

CopilotAI 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

This PR refactors the Vercel Queue message lifetime tracking by replacing the custom messageQueuedAt field with the built-in meta.createdAt timestamp provided by the Vercel Queue library. It also introduces environment variable overrides for testing the 24-hour message TTL behavior.

Key Changes:

  • Simplified message lifetime tracking by using Vercel's native meta.createdAt instead of manually tracking message queue timestamps
  • Added environment variable configuration (VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER) to allow testing different TTL scenarios
  • Improved logging for message lifetime limit handling and timeout clamping

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/queue.tsRemoved custom messageQueuedAt tracking; added env var configuration for testing; simplified message lifetime calculation using meta.createdAt; improved logging for debugging
packages/world-vercel/src/queue.test.tsUpdated tests to use meta.createdAt in mock objects; removed backwards compatibility test; updated test descriptions to reflect new behavior
Comments suppressed due to low confidence (1)

packages/world-vercel/src/queue.test.ts:196

  • Consider adding tests that verify the environment variable overrides work correctly. Since the PR title mentions "Add env vars to help test Vercel Queue max 24 hour message TTL", it would be valuable to have at least one test that demonstrates setting VERCEL_QUEUE_MESSAGE_LIFETIME and VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER environment variables and verifies the behavior changes accordingly.
describe('createQueue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('queue()', () => {
it('should send message with payload and queueName', async () => {
mockSend.mockResolvedValue({ messageId: 'msg-123' });
const queue = createQueue();
await queue.queue('__wkf_workflow_test', { runId: 'run-123' });
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
});
describe('createQueueHandler()', () => {
// Helper to simulate handleCallback behavior
function setupHandler(handlerResult: { timeoutSeconds: number } | void) {
const capturedHandlers: Record<
string,
{ default: (body: unknown, meta: unknown) => Promise<unknown> }
> = {};
mockHandleCallback.mockImplementation((handlers) => {
Object.assign(capturedHandlers, handlers);
return async (req: Request) => new Response('ok');
});
const queue = createQueue();
queue.createQueueHandler('__wkf_workflow_', async () => handlerResult);
// Get the handler that was registered
const handlerKey = Object.keys(capturedHandlers)[0];
return capturedHandlers[handlerKey].default;
}
it('should pass through timeoutSeconds when message is fresh', async () => {
const handler = setupHandler({ timeoutSeconds: 50000 });
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
// Should pass through unchanged since message is fresh
expect(result).toEqual({ timeoutSeconds: 50000 });
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue
});
it('should clamp timeoutSeconds when message has limited lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 22 hours ago
// maxAllowedTimeout = 86400 - 3600 - 79200 = 3600s (1 hour)
const oldMessageTime = new Date(Date.now() - 22 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should clamp to maxAllowedTimeout (~3600s)
expect(result).toBeDefined();
expect((result as { timeoutSeconds: number }).timeoutSeconds).toBeCloseTo(
3600,
0
);
expect(mockSend).not.toHaveBeenCalled(); // No re-enqueue, just clamping
});
it('should re-enqueue when message has no lifetime remaining', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Message that was created 23 hours ago (at the buffer limit)
// maxAllowedTimeout = 86400 - 3600 - 82800 = 0s
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
// Should return undefined (acknowledge old message)
expect(result).toBeUndefined();
// Should have re-enqueued
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual({ runId: 'run-123' });
expect(sentPayload.queueName).toBe('__wkf_workflow_test');
});
it('should not re-enqueue when message has enough lifetime remaining', async () => {
const handler = setupHandler({ timeoutSeconds: 7200 }); // 2 hours
// Message that was created 10 hours ago (plenty of time remaining)
const messageTime = new Date(Date.now() - 10 * 60 * 60 * 1000);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: messageTime }
);
// Should return the timeout (not re-enqueue)
expect(result).toEqual({ timeoutSeconds: 7200 });
expect(mockSend).not.toHaveBeenCalled();
});
it('should pass through result when no timeoutSeconds', async () => {
const handler = setupHandler(undefined);
const result = await handler(
{
payload: { runId: 'run-123' },
queueName: '__wkf_workflow_test',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: new Date() }
);
expect(result).toBeUndefined();
expect(mockSend).not.toHaveBeenCalled();
});
it('should handle step payloads correctly', async () => {
mockSend.mockResolvedValue({ messageId: 'new-msg-123' });
const handler = setupHandler({ timeoutSeconds: 3600 }); // 1 hour
// Old message approaching expiry
const oldMessageTime = new Date(Date.now() - 23 * 60 * 60 * 1000);
const stepPayload = {
workflowName: 'test-workflow',
workflowRunId: 'run-123',
workflowStartedAt: Date.now(),
stepId: 'step-456',
};
await handler(
{
payload: stepPayload,
queueName: '__wkf_step_myStep',
},
{ messageId: 'msg-123', deliveryCount: 1, createdAt: oldMessageTime }
);
expect(mockSend).toHaveBeenCalledTimes(1);
const sentPayload = mockSend.mock.calls[0][1];
expect(sentPayload.payload).toEqual(stepPayload);
});
});
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +21
const VERCEL_QUEUE_MESSAGE_LIFETIME = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME ?? 86400 // 24 hours in seconds
);
const MESSAGE_LIFETIME_BUFFER = Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER ?? 3600 // 1 hour buffer before lifetime expires

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

Using Number() to convert environment variables can produce NaN if the environment variables contain invalid numeric values. This would cause the message lifetime calculations to fail silently. Consider adding validation to ensure the environment variables are valid numbers, or use parseInt() with explicit radix and validate the result.

Suggested change
constVERCEL_QUEUE_MESSAGE_LIFETIME=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME??86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=Number(
process.env.VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER??3600// 1 hour buffer before lifetime expires
functiongetNumericEnv(name: string,defaultValue: number): number{
constraw=process.env[name];
if(raw==null||raw===''){
returndefaultValue;
}
constparsed=parseInt(raw,10);
if(Number.isNaN(parsed)){
console.warn(
`Invalid numeric value for environment variable ${name}: "${raw}". Using default value ${defaultValue}.`
);
returndefaultValue;
}
returnparsed;
}
constVERCEL_QUEUE_MESSAGE_LIFETIME=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME',
86400// 24 hours in seconds
);
constMESSAGE_LIFETIME_BUFFER=getNumericEnv(
'VERCEL_QUEUE_MESSAGE_LIFETIME_BUFFER',
3600// 1 hour buffer before lifetime expires

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +79
const maxAllowedTimeout =
VERCEL_QUEUE_MESSAGE_LIFETIME -
MESSAGE_LIFETIME_BUFFER -
messageAge;

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The maxAllowedTimeout calculation can result in negative values, which are then checked with <= 0. However, when maxAllowedTimeout is negative, using it in calculations or logging could be confusing. Consider clamping maxAllowedTimeout to a minimum of 0 using Math.max(0, ...) to make the logic clearer, even though the conditional already handles negative values.

Suggested change
constmaxAllowedTimeout=
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge;
constmaxAllowedTimeout=Math.max(
0,
VERCEL_QUEUE_MESSAGE_LIFETIME-
MESSAGE_LIFETIME_BUFFER-
messageAge
);

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
// Calculate how old this message is using the queue's createdAt timestamp
const messageAge = (now - meta.createdAt.getTime()) / 1000; // Convert to seconds

CopilotAIDec 19, 2025

Copy link

Choose a reason for hiding this comment

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

The code assumes meta.createdAt is always available and is a valid Date object. If the Vercel Queue library doesn't guarantee this field is always present, or if it could be null/undefined, the code will throw an error when calling .getTime(). Consider adding validation or a fallback to handle cases where meta.createdAt might be missing, or verify that the Vercel Queue library guarantees this field is always present.

Suggested change
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-meta.createdAt.getTime())/1000;// Convert to seconds
constcreatedAt=meta?.createdAt;
if(!(createdAtinstanceofDate)){
console.warn(
'[Workflows] meta.createdAt is missing or not a Date; skipping message lifetime calculations.'
);
returnresult;
}
// Calculate how old this message is using the queue's createdAt timestamp
constmessageAge=(now-createdAt.getTime())/1000;// Convert to seconds

Copilot uses AI. Check for mistakes.
TooTallNate added a commit that referenced this pull request Dec 19, 2025
* Add env vars to help test Vercel Queue max 24 hour message TTL
* .
* .
* .
Sign up for freeto 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

@TooTallNate