2024-08-24 by Roel Kristelijn
When your production application suddenly starts throwing Error 1102: Worker exceeded resource limits, it's time for some serious performance detective work. This is the story of how we diagnosed, analyzed, and solved a critical resource limit issue in our Next.js blog deployed on Cloudflare Workers.
Error 1102 Ray ID: 97444d3e0ece590c • 2025-08-24 16:42:14 UTC Worker exceeded resource limits
Our blog suddenly became inaccessible, throwing this cryptic error. Users couldn't access any pages, and the Worker was consistently hitting resource limits.
Cloudflare Workers have strict resource constraints:
| Resource | Free Plan | Paid Plan |
|---|---|---|
| CPU Time | 10ms | 50ms |
| Memory | 128MB | 128MB |
| Bundle Size | 1MB | 10MB |
| Subrequests | 50 | 1000 |
When any of these limits are exceeded, you get Error 1102.
Let's examine what was happening in our application:
# Check bundle size ls -lh src/data/posts.json # Output: -rw-r--r-- 1 staff 226K Aug 24 13:33 posts.json
226KB for a single JSON file! This was our first red flag.
Here's what was happening on every request:
Our posts.json contained:
{ "posts": [ { "id": "01-creating-nextjs-project", "slug": "creating-nextjs-project...", "title": "Creating a Next.js Project", "excerpt": "Brief description...", "content": "# Very long content with Mermaid diagrams, code blocks, etc..." } // ... 18 more posts with full content ] }
Problem: Every page load imported the entire 226KB file, even when only metadata was needed.
| Metric | Impact | Consequence |
|---|---|---|
| Bundle Size | 226KB loaded on import | Slow Worker startup |
| Memory Usage | All posts in memory | High memory pressure |
| CPU Time | JSON parsing + processing | Exceeded 10ms limit |
| Network | Large bundle transfer | Increased latency |
We implemented a two-tier data architecture:
// scripts/optimize-for-workers.js async function optimizeForWorkers() { const postsData = JSON.parse(fs.readFileSync(POSTS_FILE, 'utf8')); // Create lightweight metadata const metadata = { posts: postsData.posts.map(post => ({ id: post.id, slug: post.slug, title: post.title, excerpt: post.excerpt, date: post.date, author: post.author, // Remove content field })), slugs: postsData.slugs, generatedAt: postsData.generatedAt }; // Save individual content files for (const post of postsData.posts) { const contentFile = path.join(CONTENT_DIR, `${post.slug}.json`); fs.writeFileSync(contentFile, JSON.stringify({ content: post.content })); } fs.writeFileSync(METADATA_FILE, JSON.stringify(metadata, null, 2)); }
// src/lib/posts-static.ts import postsMetadata from '@/data/posts-metadata.json'; // Only 10.9KB! const contentCache = new Map<string, string>(); async function loadPostContent(slug: string): Promise<string> { if (contentCache.has(slug)) { return contentCache.get(slug)!; } try { // Dynamic import - only loads when needed const contentModule = await import(`@/data/content/${slug}.json`); const content = contentModule.content || ''; contentCache.set(slug, content); return content; } catch (error) { console.error(`Error loading content for ${slug}:`, error); return ''; } } // Metadata only - fast and lightweight export function getAllPosts(): Omit<Post, 'content'>[] { return postsMetadata.posts.map(post => ({ id: post.id, slug: post.slug, title: post.title, date: post.date, author: post.author, excerpt: post.excerpt, })); } // Full post with content - loaded on demand export async function getPostBySlug(slug: string): Promise<Post | undefined> { const postMeta = postsMetadata.posts.find(post => post.slug === slug); if (!postMeta) return undefined; const content = await loadPostContent(slug); return { ...postMeta, content }; }
# Before optimization Original size: 226.3KB # After optimization Metadata size: 10.9KB Space saved: 95.2% (215.4KB)
| Scenario | Before | After | Improvement |
|---|---|---|---|
| Posts listing | 226KB | 10.9KB | 95.2% reduction |
| Single post | 226KB | 10.9KB + ~12KB | ~90% reduction |
| Multiple posts | 226KB | 10.9KB + (n × ~12KB) | Scales linearly |
| Component | Before | After | Savings |
|---|---|---|---|
| Metadata | 226KB | 10.9KB | 215.1KB |
| Content | Included | On-demand | Variable |
| Total Initial | 226KB | 10.9KB | 95.2% |
// Posts listing: Only metadata needed const posts = getAllPosts(); // 10.9KB loaded // Individual post: Metadata + specific content const post = await getPostBySlug('my-post'); // 10.9KB + ~12KB
Problem: Cloudflare Workers have limitations with dynamic imports.
Solution: Use static imports with dynamic paths that are known at build time:
// This works in Workers const contentModule = await import(`@/data/content/${slug}.json`); // This doesn't work in Workers const contentModule = await import(dynamicPath);
Problem: Posts without content need different types.
Solution: Flexible type definitions:
interface PostCardProps { post: Omit<Post, 'content'> | Post; // Supports both }
Problem: Need to run optimization automatically.
Solution: Integrated build pipeline:
{ "scripts": { "prebuild": "node scripts/generate-posts-data.js && node scripts/optimize-for-workers.js" } }
# Successful deployment output ✨ Success! Uploaded 7 files (66 already uploaded) (1.77 sec) Total Upload: 13450.13 KiB / gzip: 2695.83 KiB Worker Startup Time: 24 ms Deployed next-blog triggers (1.27 sec) https://next-blog.rkristelijn.workers.dev
| Metric | Before | After | Improvement |
|---|---|---|---|
| Error Rate | 100% (Error 1102) | 0% | ✅ Resolved |
| Bundle Size | 226KB | 10.9KB | 95.2% reduction |
| Memory Usage | High | Low | ~95% reduction |
| Startup Time | Slow | Fast | Significantly improved |
| Scalability | Limited | High | Linear scaling |
# Add to CI/CD pipeline npm run build | grep "First Load JS"
Don't load everything upfront. Design for on-demand loading:
// Good: Load what you need const metadata = getPostMetadata(); // Bad: Load everything const allData = getAllDataIncludingContent();
// For listings: Metadata only interface PostSummary { id: string; title: string; excerpt: string; date: string; } // For details: Full content interface PostDetail extends PostSummary { content: string; }
// Cache expensive operations const contentCache = new Map<string, string>(); // But don't cache everything // Cache only frequently accessed content
// Add performance monitoring console.time('operation'); await expensiveOperation(); console.timeEnd('operation'); // Monitor in production if (process.env.NODE_ENV === 'production') { // Log performance metrics }
# Gzip content files gzip content/*.json # Potential 60-80% additional savings
// Cache content at Cloudflare edge const cacheKey = `post-content-${slug}`; const cached = await caches.default.match(cacheKey);
// Load content sections progressively const sections = await loadPostSections(slug);
// Client-side caching for repeat visits self.addEventListener('fetch', event => { if (event.request.url.includes('/content/')) { event.respondWith(cacheFirst(event.request)); } });
The Error 1102 "Worker exceeded resource limits" taught us valuable lessons about performance optimization in serverless environments. By implementing lazy loading and reducing our bundle size by 95.2%, we not only solved the immediate problem but also created a more scalable architecture.
The solution demonstrates that with careful analysis and strategic optimization, even complex applications can run efficiently within Cloudflare Worker constraints while maintaining excellent performance and user experience.
Our blog now handles traffic smoothly, scales efficiently, and stays well within resource limits - proving that sometimes the best optimization is simply not loading what you don't need.
Performance Stats:
The complete optimization code and scripts are available in our GitHub repository.