Netlify Scheduled Functions Debugging Guide 2026
Debug Netlify scheduled functions with real error patterns, production code, and exact troubleshooting steps for indie hackers.
Netlify Scheduled Functions Debugging Guide 2026
TL;DR: Netlify scheduled functions (cron jobs) fail silently. Enable verbose logging via netlifyignore, check execution logs in UI, use context.clientContext for debugging, handle timezone issues with explicit UTC, and always set function timeouts above your longest operation. Most issues stem from missing environment variables or exceeded execution limits.
The Silent Failure Problem
Scheduled functions on Netlify are powerful—but they fail quietly. Unlike HTTP functions that return errors to clients, scheduled functions execute in the background. Your cron job runs, silently returns undefined, and you never know it failed until your data pipeline breaks.
As of Netlify Functions runtime 2.12.x (verify in [official docs](https://docs.netlify.com/functions/overview/)), scheduled function debugging requires intentional setup. Let's cover the exact patterns that work.
Real Error Messages You'll See
Here are three common console errors when things go wrong:
``` Error: ENOMEM: Cannot allocate memory, spawn ``` This means your function exceeded available memory or spawned too many processes.
``` Task timed out after 900000 milliseconds ``` Your function exceeded the 15-minute execution limit (900 seconds for paid plans; verify current limits in [official docs](https://docs.netlify.com/functions/overview/?fn-timeout)).
``` Error: getaddrinfo ENOTFOUND api.external-service.com ``` Network call failed—usually DNS resolution during cold starts or VPC isolation issues.
Setting Up Logging for Scheduled Functions
First, understand what context you receive. Scheduled functions trigger via the schedule property in netlify.toml:
```toml [functions] node_bundler = "esbuild"
[[functions]] name = "daily-digest" schedule = "0 9 * * *" ```
Your function receives a context object—but not event. Log aggressively:
```javascript
// functions/daily-digest.ts
export default async (req: Request, context: Context) => {
const executionId = exec_${Date.now()}_${Math.random().toString(36).slice(2)};
console.log([${executionId}] Scheduled function triggered);
console.log([${executionId}] Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone});
console.log([${executionId}] Environment: ${process.env.NODE_ENV});
console.log([${executionId}] Available env vars: ${Object.keys(process.env).filter(k => !k.includes('SECRET')).join(', ')});
try {
console.log([${executionId}] Starting digest generation);
const data = await fetchUserData();
console.log([${executionId}] Fetched ${data.length} records);
await sendDigests(data);
console.log([${executionId}] Digests sent successfully);
return new Response(
JSON.stringify({ success: true, executionId, recordsProcessed: data.length }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error([${executionId}] FATAL ERROR: ${errorMsg});
console.error([${executionId}] Stack: ${error instanceof Error ? error.stack : 'N/A'});
// Return 200 with error details—Netlify treats non-2xx as failures
return new Response(
JSON.stringify({ success: false, executionId, error: errorMsg }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
};
```
Critical: Always return status: 200. Netlify interprets 4xx/5xx responses as execution failures and may retry.
Timezone Gotchas
Schedules in netlify.toml use UTC, but your function's new Date() may run in a different timezone during execution. Always be explicit:
```javascript // Get current UTC time const nowUTC = new Date(); const utcHours = nowUTC.getUTCHours(); const utcMinutes = nowUTC.getUTCMinutes();
// Convert to specific timezone for logic const formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', hour: '2-digit', minute: '2-digit', hour12: false, }); const [hours, minutes] = formatter.format(nowUTC).split(':');
console.log(UTC: ${utcHours}:${utcMinutes} | EST: ${hours}:${minutes});
```
Accessing Execution Logs
Two ways to view scheduled function logs:
Via Netlify UI:
1. Netlify dashboard → Functions → Select function → "Invocations" tab
2. Click any invocation to see full logs
3. Look for your executionId patterns
Via Netlify CLI (v17.0+): ```bash netlify functions:invoke daily-digest --identity netlify logs --function=daily-digest ```
Note: CLI logs may be delayed by 30-60 seconds. Verify in [official docs](https://docs.netlify.com/cli/get-started/?fn-logs) for current CLI version.
Environment Variables in Scheduled Functions
Scheduled functions cannot access secrets through context.clientContext (that's for edge functions). Use standard Node.js process.env:
```javascript export default async (req: Request, context: Context) => { const apiKey = process.env.EXTERNAL_API_KEY; // Set in Netlify UI const dbUrl = process.env.DATABASE_URL; // Build-time variable if (!apiKey) { console.error('Missing EXTERNAL_API_KEY'); return new Response( JSON.stringify({ success: false, error: 'Missing credentials' }), { status: 200 } ); } // Continue with apiKey... }; ```
Timeout Configuration
Default function timeout is 10 seconds (standard plan). Increase via netlify.toml:
```toml [[functions]] name = "long-running-job" schedule = "0 2 * * *" timeout = 60 # 60 seconds for paid plans ```
Verify maximum allowed in [official docs](https://docs.netlify.com/functions/overview/?fn-timeout) for your plan level.
Production-Ready Scheduled Function Template
```typescript interface Context { clientContext?: Record<string, unknown>; }
export default async (req: Request, context: Context): Promise<Response> => {
const execId = ${Date.now()};
const startTime = performance.now();
try {
console.log([${execId}] Started at ${new Date().toISOString()});
// Your business logic
const result = await runDailyJob();
const duration = ((performance.now() - startTime) / 1000).toFixed(2);
console.log([${execId}] Completed in ${duration}s);
return new Response(
JSON.stringify({ success: true, result, durationSec: duration }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error([${execId}] Failed: ${msg});
return new Response(
JSON.stringify({ success: false, error: msg }),
{ status: 200 }
);
}
};
```
Related Guides
What am I missing?
Has your scheduled function failed in unexpected ways? Did you solve a timeout issue differently? Comment below with your debugging discoveries—let's build a community knowledge base for Netlify scheduled functions. Specific edge cases around database connections, webhook retries, or multi-region execution are especially valuable.