Home /
GitHub Pages limits: build a browser tool that fits
GitHub Pages size, bandwidth, build and use limits, with a complete JSON formatter tested locally at root and project-path URLs.
- Pages serves static files and does not run a backend for the tool.
- Hard site-size limits and soft bandwidth/build limits are different.
- Relative asset paths keep this one-page tool inside the repository URL.
GitHub Pages can host a small JavaScript tool for free, provided the tool can run in the browser. It will serve the HTML, CSS and JavaScript; it will not run a Python script or keep an API secret for you.
The important GitHub Pages limits are a 1 GB published-site maximum, 100 GB/month soft bandwidth limit, 10 builds/hour soft limit and 10-minute deployment timeout. The build-frequency limit does not apply to a custom GitHub Actions publishing workflow. Those are different kinds of constraint, not a single allowance that turns into an overage bill. GitHub’s limits page describes the conditions and what can happen when a site exceeds them.
For a personal JSON formatter, these quotas are unlikely to be the first obstacle. The architecture and file paths matter sooner. Here is a complete example, including the small path mistake that makes a tool work locally and fail at username.github.io/repository/.
The documentation was checked on 27 October 2026. The example was exercised in local Chromium, at both a root URL and a repository-style subdirectory. It has not been deployed to GitHub Pages.
The GitHub Pages limits that affect this tool
On GitHub Free, the source repository must be public. Keep secrets out of the repository and the delivered JavaScript. A private repository on a paid plan does not automatically make its Pages site private.
The usage restrictions also matter. Pages is not allowed as free hosting for an online business, e-commerce site, or a site primarily facilitating commercial transactions or providing commercial SaaS. A personal or open-source utility is the example here, not a recommendation to put a paid service on Pages.
| Limit | What it changes for this tool |
|---|---|
| Published site: at most 1 GB | Measure the total size of the files you publish. A 1 GB source repository is a separate recommendation. |
| Bandwidth: soft 100 GB/month | Downloads of the page and its assets use bandwidth. Formatting already-loaded text does not download another copy of the app. |
| Builds: soft 10/hour | Relevant when updating the site. Custom Actions publishing has an exception to this particular limit. |
| Deployment: 10-minute timeout | Keep the publishing step small. This example needs no build. |
| Rate limiting | GitHub may return HTTP 429. A soft quota is not an availability promise. |
A service with accounts, shared saved results or a server-side secret needs something beyond these static files. The free-compute guide separates those backend choices from static hosting.
Three files, with no backend
Create a directory named demo-tool:
demo-tool/
index.html
app.js
style.css
The formatter accepts pasted JSON or a local file, limits input to 100 KiB, and displays the result in a read-only text box. It has no external libraries or upload endpoint. The size cap is a choice in this example, not a GitHub Pages upload allowance.
Put this in index.html. The two ./ paths are deliberate.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Small JSON formatter</title>
<link rel="stylesheet" href="./style.css">
<script src="./app.js" defer></script>
</head>
<body>
<main>
<h1>Small JSON formatter</h1>
<p>Paste JSON or choose a local file, up to 100 KiB. This example formats
it in your browser; its code does not upload the contents.</p>
<p>Loading this page still makes requests to its host, which can keep
request and IP logs.</p>
<form id="formatter">
<label for="file">Choose a JSON file (optional)</label>
<input id="file" type="file" accept=".json,application/json" aria-describedby="limit">
<p id="limit">Maximum input: 102,400 bytes (100 KiB), including pasted text.</p>
<label for="input">JSON input</label>
<textarea id="input" rows="9" spellcheck="false" autocapitalize="off"
autocomplete="off" aria-describedby="limit status"></textarea>
<div class="actions">
<button type="submit">Format JSON</button>
<button type="reset">Reset</button>
</div>
</form>
<p id="status" role="status" aria-live="polite" aria-atomic="true">Ready.</p>
<label for="output">Formatted JSON</label>
<textarea id="output" rows="12" readonly spellcheck="false"></textarea>
<noscript><p>Enable JavaScript to use this formatter.</p></noscript>
</main>
</body>
</html>
Then add app.js:
const form = document.querySelector('#formatter');
const input = document.querySelector('#input');
const output = document.querySelector('#output');
const fileInput = document.querySelector('#file');
const status = document.querySelector('#status');
const maxBytes = 100 * 1024;
let readVersion = 0;
function showError(message) {
output.value = '';
input.setAttribute('aria-invalid', 'true');
status.textContent = message;
}
function formatJSON() {
if (new TextEncoder().encode(input.value).length > maxBytes) {
showError('Input is too large. Use at most 100 KiB (102,400 bytes).');
return;
}
try {
output.value = JSON.stringify(JSON.parse(input.value), null, 2);
input.removeAttribute('aria-invalid');
status.textContent = 'JSON formatted successfully.';
} catch (error) {
showError(`Invalid JSON: ${error.message}`);
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
formatJSON();
});
fileInput.addEventListener('change', async () => {
const version = ++readVersion;
const file = fileInput.files[0];
if (!file) return;
output.value = '';
if (file.size > maxBytes) {
showError('File is too large. Choose a file of at most 100 KiB.');
return;
}
status.textContent = 'Reading local file…';
try {
const text = await file.text();
if (version !== readVersion) return;
input.value = text;
formatJSON();
} catch {
if (version === readVersion) showError('Could not read this file. Try another file.');
}
});
input.addEventListener('input', () => {
++readVersion;
output.value = '';
input.removeAttribute('aria-invalid');
status.textContent = 'Input changed. Select Format JSON to update the result.';
});
form.addEventListener('reset', () => {
++readVersion;
output.value = '';
input.removeAttribute('aria-invalid');
status.textContent = 'Reset. Ready for new JSON.';
input.focus();
});
Finally, style.css:
:root { color-scheme: light dark; font: 1rem/1.5 system-ui, sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; }
main { width: min(100% - 2rem, 52rem); margin: 2rem auto; }
h1 { line-height: 1.15; }
label { display: block; font-weight: 650; margin-top: 1rem; }
input, textarea, button { font: inherit; }
input[type="file"] { max-width: 100%; margin-top: .5rem; }
textarea { display: block; width: 100%; padding: .75rem; resize: vertical;
font-family: ui-monospace, monospace; border: 1px solid #888; border-radius: .3rem; }
textarea[aria-invalid="true"] { border: 2px solid #bf3535; }
.actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1rem; }
button { min-height: 44px; padding: .5rem 1rem; cursor: pointer; }
:focus-visible { outline: 3px solid #177bca; outline-offset: 3px; }
#status { min-height: 1.5em; overflow-wrap: anywhere; }
#limit { font-size: .9rem; }
@media (max-width: 30rem) { main { margin-top: 1rem; } }
The output uses a text box’s value, so a string containing <img> stays text. It is not inserted as executable HTML. The version counter prevents a late file read from replacing input after a reset or another edit.
This is a formatter built on JavaScript’s JSON values, not a lossless editor. Very large integers can lose precision: 9007199254740993 becomes 9007199254740992 in the checked example. Repeated object keys and the original number formatting are not preserved either. Use a parser designed to preserve those details if they matter to your data.
Reproduce the project-path problem locally
First serve the folder itself. With Python 3 installed, run this from inside demo-tool:
python3 -m http.server 8000 --bind 127.0.0.1
Open http://127.0.0.1:8000/, format {"hello":"world"}, then try invalid input such as {broken. Check that Reset clears the input, output and error.
Stop the server with Ctrl+C. Move to the parent directory and run the same command again. Now open http://127.0.0.1:8000/demo-tool/. This second URL is the important test: a Pages project site normally lives under the repository name rather than the domain root.
At that URL, the browser resolves paths like this:
| Reference in the page | Requested path |
|---|---|
./app.js | /demo-tool/app.js |
/app.js | /app.js |
A leading slash starts at the origin’s root. It skips demo-tool. If there happens to be a different file at the root, the mistake can even load the wrong script rather than produce a useful 404.
The local check included a server that exposed assets only under /demo-tool/. Changing just the stylesheet and script references to leading-slash paths produced two 404s. Restoring ./style.css and ./app.js loaded both files and restored the formatter. This example has one page; nested pages or a framework build need their own base-path configuration.
The same browser run checked pasted text, file input, malformed JSON, the size cap, keyboard reset and HTML-looking strings at both URL locations. No additional network requests were observed during those input operations. The 390-pixel viewport check found no page-wide horizontal overflow. That is desktop browser automation, not a physical-phone or screen-reader test.
Publish the folder when you are ready
The documented branch-publishing route is enough for these files:
- Put the three files at the root of a suitable public repository. Add an empty
.nojekyllfile if you want branch publishing to bypass Jekyll processing. - In the repository’s Settings → Pages, choose Deploy from a branch.
- Select the branch containing the files and the /(root) folder, then save.
- Once publishing completes, use Visit site and repeat the input and path checks at the actual project URL.
These are documentation-based instructions; the work described here stopped at local checks. No Pages setting was changed and no hosted deployment was tested.
If the page returns 404, check the publishing source and entry file before changing the JavaScript. index.html belongs at the top of the selected folder, and the filename is case-sensitive. If the page loads but the tool is inert or unstyled, look for failed app.js or style.css requests in the browser’s Network panel. GitHub’s 404 guide covers the publishing and domain cases that a local server cannot establish.
This formatter does not upload the JSON you paste or select. That is a property of these three files, not a guarantee about other tools hosted on Pages. Loading the app still contacts GitHub, which logs visitors’ IP addresses for security.