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"
StepTimeNote
Netlify cache restore + npm ci2 min 40 snpm ci deletes node_modules restored from cache
Tailwind CSS build25 s—
Hugo: image processing11 min 30 sresources/_gen not cached between builds
Hugo: rendering 4,200 pages48 s—
Netlify post-processing (asset optimisation)1 min 10 sEnabled years ago, redundant
Total16 min 33 sOver the limit on busy days
Netlify build time by step before the fixes Horizontal bars in seconds. Hugo image processing 690 seconds. npm ci after cache restore 160 seconds. Netlify post-processing 70 seconds. Hugo rendering 48 seconds. Tailwind 25 seconds. Where the 16.5 minutes went Hugo image processing 690 s npm ci 160 s Post-processing 70 s Hugo rendering 48 s Tailwind 25 s
Timestamps from the Netlify deploy log, median of five builds; image processing was 70 percent of the build.

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.

Seconds per build step before and after the fixes Paired bars. Image processing went from 690 to 22 seconds. Dependency install from 160 to 18. Post-processing from 70 to 0. Hugo rendering stayed at 48. Tailwind stayed at 25. Build time per step, before and after Before After Image processing 690 s 22 s Dependency install 160 s 18 s Post-processing 70 s 0 s Hugo rendering 48 s 48 s Tailwind 25 s 25 s
Median of ten builds after the fixes, typical content change, cache warm.

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.

Decision tree for a Netlify build that exceeds the time limit The root asks where the build spends its time. Image or asset processing points to caching the generator's output between builds. Dependency installs point to not running npm ci after Netlify's cached install. Commits that do not change the site point to an ignore command. A cold build that still cannot fit points to building in external CI and deploying with the Netlify CLI. Choosing the fix Where does the build spend its time? Image processing Cache resources build plugin Dependency install Use Netlify's install drop npm ci Unneeded builds ignore command skip by path Cold build too slow Build in CI deploy with CLI
Asking Netlify for a higher limit is last; it keeps every deploy slow.

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

MeasureBeforeAfterTool
Typical build (warm cache)16 min 33 s3 min 40 sNetlify deploy log
Cold build17 min 10 s (often cancelled)12 min 05 sNetlify deploy log
Builds cancelled per month90Deploy history
Builds skipped (no site changes)0~35% of commitsDeploy history
Build minutes per month~2,900~420Netlify 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 ci after 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.

Filed underFixesNetlify