Fixing Netlify Builds That Exceed the Time Limit
The build log ends abruptly: "Build exceeded maximum allowed runtime." Netlify cancels builds that run past the plan's limit — 15 minutes by default — and the site stays on its previous deploy. It usually starts on a busy day: a large content import, a new set of product photos, a dependency upgrade that invalidated the cache. Once builds hover near the limit, every deploy becomes a gamble.
This guide brings a Hugo and Tailwind marketing site with a large image library from 16–18 minute builds (with intermittent cancellations) to under 4 minutes on Netlify, by finding where the time went and caching or skipping it. It belongs to Netlify vs Vercel Deployment Strategies; Netlify's caching options are compared with Vercel's in Build Caching on Netlify and Vercel.
Step 1: Find Where the Time Goes
Netlify's deploy log has timestamps per line. Adding timing around each step of the build command made the breakdown explicit:
# netlify.toml build command
command = "time npm ci && time npm run css && time hugo --gc --minify --templateMetrics | tail -20"
| Step | Time | Note |
|---|---|---|
Netlify cache restore + npm ci | 2 min 40 s | npm ci deletes node_modules restored from cache |
| Tailwind CSS build | 25 s | — |
| Hugo: image processing | 11 min 30 s | resources/_gen not cached between builds |
| Hugo: rendering 4,200 pages | 48 s | — |
| Netlify post-processing (asset optimisation) | 1 min 10 s | Enabled years ago, redundant |
| Total | 16 min 33 s | Over the limit on busy days |
Fix 1: Cache Hugo's Processed Images
Hugo writes processed images to resources/_gen. Netlify does not keep it between builds, so every build reprocessed all 3,100 images into four widths and two formats. A small local build plugin saves and restores it with Netlify's cache utilities:
// plugins/hugo-cache/index.js
export const onPreBuild = async ({ utils }) => { await utils.cache.restore('./resources'); };
export const onPostBuild = async ({ utils }) => { await utils.cache.save('./resources'); };
# netlify.toml
[[plugins]]
package = "/plugins/hugo-cache"
With the cache restored, Hugo only processes new or changed images. Image processing fell from 11 minutes 30 seconds to 22 seconds on a typical build. Hugo Image Processing Cache in CI explains the cache's structure.
Fix 2: Stop npm ci Throwing Away the Cache
Netlify restores node_modules from cache, then the build command ran npm ci, which deletes node_modules and reinstalls from scratch — 2 minutes 40 seconds wasted. Netlify runs its own install step (npm install) before the build command when it detects a lockfile; removing npm ci from the command let that cached install do the work. For strictness, NPM_FLAGS="--prefer-offline --no-audit --no-fund" keeps it fast. Install time fell to 18 seconds.
Fix 3: Remove Redundant Post-Processing
Netlify's legacy asset optimisation (bundling and minifying CSS and JavaScript, compressing images after the build) was enabled from the site's early days. Hugo already minified everything and the images were already optimised. The post-processing re-did that work for 70 seconds and occasionally changed fingerprinted file contents, which broke subresource integrity hashes. It was turned off in the site settings.
Fix 4: Skip Builds That Change Nothing
About a third of builds were triggered by commits that touched only files that do not affect the site — README edits, CI configuration, design source files. Netlify's ignore command decides whether to build:
[build]
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- content/ layouts/ assets/ static/ data/ config/ package-lock.json"
If none of those paths changed, the command exits 0 and Netlify skips the build entirely.
When the Cache Is Cold
A cold cache — after clearing it, or after a Hugo upgrade that changes image processing output — still processes every image, which took 12 minutes. That is under the limit but close. Two safeguards keep a cold build from failing:
- Image processing parameters are pinned in a partial, so Hugo upgrades rarely invalidate the cache; when they do, the upgrade pull request's preview build warms a fresh cache before production needs it.
- New images are pre-sized when they are added: a pre-commit hook resizes any image over 3,000 pixels wide, which cut cold processing time by 40% because many originals had been 6,000-pixel camera exports.
Alternative: Build in CI, Deploy to Netlify
If a site's cold build cannot fit the limit, building elsewhere removes the constraint. GitHub Actions builds the site with its own caches and deploys with the Netlify CLI:
- run: npx netlify-cli deploy --dir=public --prod
env: { NETLIFY_AUTH_TOKEN: '${{ secrets.NETLIFY_AUTH_TOKEN }}', NETLIFY_SITE_ID: '${{ secrets.NETLIFY_SITE_ID }}' }
Netlify then only receives files. This site did not need it after the fixes, but its sister site — 40,000 product images — uses this setup.
Watching Build Time After the Fix
Build time creeps back as content grows. A short step at the end of the build command records the total build duration and the number of images processed to a JSON file in the deploy, and a weekly job reads the last 50 deploys' files from the Netlify API and posts a chart to the team channel. When the warm build passed 5 minutes for a week, the cause was a new folder of 4K screenshots that the pre-commit resize hook did not cover; adding the folder to the hook's paths brought it back to 3 minutes 50 seconds. The same data shows how close a cold build is to the limit, which is the number that decides when to move the build to external CI.
Tailwind and Other Small Steps
With the large steps fixed, the remaining minute was spread across Tailwind (25 seconds) and Hugo rendering (48 seconds). Moving Tailwind to its standalone CLI binary, cached between builds, saved 15 seconds; Hugo rendering was left alone, since profiling with --templateMetrics showed no single template worth optimising.
Measured Impact
| Measure | Before | After | Tool |
|---|---|---|---|
| Typical build (warm cache) | 16 min 33 s | 3 min 40 s | Netlify deploy log |
| Cold build | 17 min 10 s (often cancelled) | 12 min 05 s | Netlify deploy log |
| Builds cancelled per month | 9 | 0 | Deploy history |
| Builds skipped (no site changes) | 0 | ~35% of commits | Deploy history |
| Build minutes per month | ~2,900 | ~420 | Netlify usage |
Preview builds benefited as much as production builds, because Netlify shares the build cache across branches of the same site; the first preview after a cache clear is the only slow one.
Pitfalls & Rollback
- Asking for a higher limit first. It keeps every deploy slow.
- Running
npm ciafter Netlify's cached install. It deletes the cache. - Not caching the generator's image output. It is usually most of the build.
- Redundant post-processing. Turn it off when the generator already minifies.
- Rollback: each fix is a setting or a few lines in
netlify.toml; removing the cache plugin restores the old behaviour.
Conclusion
Netlify builds that hit the time limit on static sites are usually doing the same expensive work every time. Caching Hugo's processed images, using Netlify's cached install instead of npm ci, turning off redundant post-processing and skipping builds that change nothing took this site from 16.5 minutes with regular cancellations to under 4 minutes, and cut build minutes by 85%.
FAQ
What is Netlify's build time limit?
Netlify cancels builds that run longer than the limit for the plan, which is 15 minutes by default and can be raised on some plans by request. A cancelled build does not deploy, and the previous deploy stays live. The log ends with a message that the build exceeded the maximum allowed time.
Does Netlify cache anything between builds?
Yes. Netlify restores node_modules and some framework caches automatically, and build plugins can save and restore other directories between builds through the utils cache API. Directories outside those, such as a generator's image cache, are not kept unless a plugin or the build command saves them.
Should I just ask Netlify to raise the limit?
Only after reducing the build time. A longer limit lets the same slow build pass, but every deploy still waits for it and consumes build minutes. Most long builds on static sites spend their time on image processing, dependency installs or work that could be cached or skipped.
Can I build somewhere else and deploy to Netlify?
Yes. Build in GitHub Actions or another CI system and deploy the output directory with the Netlify CLI using netlify deploy with the prod flag. The build then runs under that system's time limits and resources, and Netlify only receives files, which avoids its build limit entirely.
Related
- Parent: Netlify vs Vercel Deployment Strategies — platform choice and configuration.
- Build Caching on Netlify and Vercel — what each platform caches.
- Hugo Image Processing Cache in CI — the cache this restores.
- Fixing Vercel Builds That Run Out of Memory — the equivalent limit on Vercel.
- Netlify Build Hooks for Content Updates — builds triggered from outside Git.