Instagram Stories vanish after 24 hours and offer no download button. This script extracts the original high-resolution source file — video or photo — directly from the browser’s memory, no extensions or third-party services required.
Stories are ephemeral by design. Once they disappear, they are gone from everyone’s feed — including your own. And unlike Reels, there is no “Save” option anywhere in the UI. This script takes a completely different approach to the Reel downloader: instead of recording the screen, it reaches into the browser’s internal React data to pull the original CDN URL and opens it directly in a new tab where you can save it natively.
Using it on Desktop
Step 1 — Navigate to a Story
Go to instagram.com and open any Story. The URL must contain /stories/ — for example https://www.instagram.com/stories/username/123456/. The script validates this before doing anything.
Step 2 — Open the Browser Console
Windows / Linux: Press F12 or Ctrl + Shift + I, then click the Console tab.
Direct shortcut: Ctrl + Shift + J (Chrome) jumps straight to the Console.
Mac: Press Cmd + Option + I, then click the Console tab.
Direct shortcut: Cmd + Option + J (Chrome) jumps straight to the Console.
Step 3 — Paste and Run the Script
Paste the following code into the console and press Enter:
javascript:(function() {
console.log("🎯 Initializing Story Sniper...");
/* 1. Ensure we are actually in the Stories UI */
if (!window.location.href.includes('/stories/')) {
return alert("This sniper is calibrated for the /stories/ URL only!");
}
/* 2. X-Axis Spatial Targeting (The Pre-load Bypass) */
const allMedia = Array.from(document.querySelectorAll('video, img'));
let target = null;
let minDistance = Infinity;
const centerX = window.innerWidth / 2; /* Notice the shift to Width/X-Axis */
allMedia.sort((a, b) => (a.tagName === 'VIDEO' ? -1 : 1)).forEach(el => {
const rect = el.getBoundingClientRect();
// Measure horizontal distance to center to ignore pre-loaded off-screen stories
const distance = Math.abs(centerX - (rect.left + rect.width / 2));
// Ensure the element is visible on screen
if (rect.width > 0 && distance < minDistance && rect.left >= 0 && rect.right <= window.innerWidth) {
minDistance = distance;
target = el;
}
});
if (!target) return alert("No active story detected in the viewport!");
/* 3. The Temporal Freeze: Stop the auto-advance timer */
if (target.tagName === 'VIDEO') {
target.pause();
console.log("⏸️ Story frozen.");
}
/* 4. Surgical Fiber Scraper (Reusing our battle-tested logic) */
const videoUrls = [];
const imageUrls = [];
const collectMedia = (obj, depth = 0) => {
if (depth > 15 || !obj || typeof obj !== 'object' || obj instanceof HTMLElement) return;
for (let key in obj) {
const val = obj[key];
if (typeof val === 'string' && val.startsWith('https://') && !val.includes('<?xml')) {
if (val.includes('.mp4')) {
videoUrls.push({ url: val, area: (obj.width || 0) * (obj.height || 0) });
} else if (val.includes('.jpg') || val.includes('.webp')) {
imageUrls.push({ url: val, area: (obj.width || 0) * (obj.height || 0) });
}
} else if (val && typeof val === 'object') {
collectMedia(val, depth + 1);
}
}
};
let current = target;
while (current && videoUrls.length === 0 && imageUrls.length === 0) {
const fiberKey = Object.keys(current).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactProps$'));
if (fiberKey) collectMedia(current[fiberKey]);
current = current.parentElement;
}
/* 5. The Decision Engine */
const finalUrl = videoUrls.length > 0 ? videoUrls.sort((a,b)=>b.area-a.area)[0].url : (imageUrls.length > 0 ? imageUrls.sort((a,b)=>b.area-a.area)[0].url : null);
if (finalUrl) {
console.log("✅ High-Res Story Source Found:", finalUrl);
const link = document.createElement('a');
link.href = finalUrl;
link.target = '_blank';
link.rel = 'noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
alert("Metadata hidden. Use the 'Delta-Sync' recorder.");
}
})();
If it succeeds, the Story’s original source file opens in a new browser tab. Right-click that tab and choose Save as… to save the file to your disk. For video Stories the file is an .mp4; for photo Stories it is a .jpg or .webp.
How the Code Works
Here is a plain-English breakdown of each stage, and where it differs from the Reel downloader.
Stage 1 — URL Guard
if (!window.location.href.includes('/stories/')) {
return alert("This sniper is calibrated for the /stories/ URL only!");
}
The script immediately checks that you are on a Stories URL. The Stories UI is completely different from the Reels feed — different DOM structure, different React component tree, different media-loading strategy. Running the script on the wrong page would waste time and return garbage results, so it exits early with a helpful message.
The Reel downloader has no such guard because it targets any <video> visible in the viewport — a generic enough selector to work across Reels, profile pages, and explore. The Story Sniper is purpose-built for one specific layout.
Stage 2 — X-Axis Spatial Targeting
const centerX = window.innerWidth / 2;
allMedia.sort((a, b) => (a.tagName === 'VIDEO' ? -1 : 1)).forEach(el => {
const rect = el.getBoundingClientRect();
const distance = Math.abs(centerX - (rect.left + rect.width / 2));
if (rect.width > 0 && distance < minDistance && rect.left >= 0 && rect.right <= window.innerWidth) {
minDistance = distance;
target = el;
}
});
This is the single biggest structural difference from the Reel downloader.
The Reel downloader measures vertical position — it finds the video whose top and bottom edges are both inside window.innerHeight. That works for a vertically scrolling feed where pre-loaded videos sit above and below the current one, outside the vertical bounds of the viewport.
Stories work differently. They pre-load the previous and next stories horizontally — those elements sit to the left and right of the current story, outside the horizontal bounds of the viewport. Measuring vertical bounds would accidentally match a pre-loaded off-screen story.
The fix is to measure horizontal distance to the viewport’s centre instead. The story closest to window.innerWidth / 2 is the one the user is currently viewing. rect.left >= 0 && rect.right <= window.innerWidth confirms the element is fully within the horizontal bounds of the screen, rejecting anything hidden off to the side.
The .sort() that runs before the loop places any <video> elements ahead of <img> elements in the iteration order. Video Stories should be matched first; images are the fallback for photo Stories.
Stage 3 — The Temporal Freeze
if (target.tagName === 'VIDEO') {
target.pause();
console.log("⏸️ Story frozen.");
}
Stories auto-advance on a timer. If the script were to take a long time to scrape the fiber tree, the story could flip to the next one mid-execution, making subsequent steps target stale data. Pausing the video stops the auto-advance timer immediately. The story stays frozen while the rest of the script runs.
The Reel downloader also pauses the video, but for a different reason: it seeks to currentTime = 0 before starting MediaRecorder, and it mutes competing videos to prevent audio bleed into the recording. The Story Sniper has no recording stage at all — it just needs the story to stay still long enough to read its metadata.
Stage 4 — The React Fiber Scraper
const collectMedia = (obj, depth = 0) => {
if (depth > 15 || !obj || typeof obj !== 'object' || obj instanceof HTMLElement) return;
for (let key in obj) {
const val = obj[key];
if (typeof val === 'string' && val.startsWith('https://') && !val.includes('<?xml')) {
if (val.includes('.mp4')) {
videoUrls.push({ url: val, area: (obj.width || 0) * (obj.height || 0) });
} else if (val.includes('.jpg') || val.includes('.webp')) {
imageUrls.push({ url: val, area: (obj.width || 0) * (obj.height || 0) });
}
} else if (val && typeof val === 'object') {
collectMedia(val, depth + 1);
}
}
};
let current = target;
while (current && videoUrls.length === 0 && imageUrls.length === 0) {
const fiberKey = Object.keys(current).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactProps$'));
if (fiberKey) collectMedia(current[fiberKey]);
current = current.parentElement;
}
This is the core of the Story Sniper, and it has no equivalent in the Reel downloader.
Instagram is built on React. Every DOM element that React renders carries a hidden internal property whose name starts with __reactFiber$ or __reactProps$ (followed by a random suffix). These properties hold the component’s internal state and props — including the media URLs that React passed down from the server response.
The outer while loop walks up the DOM tree from the target <video> or <img> element, one parent at a time. At each level it checks whether the node has a React fiber key. When it finds one, it calls collectMedia to recursively search the fiber object.
collectMedia is a depth-limited recursive crawler (maximum depth 15 to avoid infinite loops on circular structures). At each level it iterates over every property of the current object. If a value is a string starting with https://, it checks whether it looks like a video (.mp4) or image (.jpg, .webp) URL and records it along with the dimensions found in the same object node — useful for picking the highest-resolution version later. If a value is itself an object (not an HTML element, which would cause infinite recursion), the function calls itself recursively.
The check for <?xml filters out SVG data URIs and XML blobs that would otherwise superficially match the https:// prefix test.
The Reel downloader never needs this because it captures the decoded video frames directly using captureStream() — it doesn’t need to know the original URL. The Story Sniper, by contrast, bypasses recording entirely by going straight to the source URL.
Stage 5 — The Decision Engine
const finalUrl = videoUrls.length > 0
? videoUrls.sort((a,b) => b.area - a.area)[0].url
: (imageUrls.length > 0 ? imageUrls.sort((a,b) => b.area - a.area)[0].url : null);
By this point videoUrls and imageUrls may each contain multiple entries — Instagram often includes multiple resolutions of the same asset in its React props. The script ranks them by area (width × height, captured during scraping) and selects the largest — the highest-resolution version available. Videos are preferred over images if both are present.
Opening the File
const link = document.createElement('a');
link.href = finalUrl;
link.target = '_blank';
link.rel = 'noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
Rather than triggering a direct download, the script opens the CDN URL in a new tab. This approach works more reliably across browsers than a programmatic download because CDN responses include content-disposition headers that direct the new tab to display the media natively. From there, right-clicking and choosing Save as… gives you the original file with no re-encoding.
The rel="noreferrer" attribute prevents Instagram’s CDN from seeing which page the request came from. The link element is appended to the DOM just long enough for .click() to fire, then removed immediately.
Story Sniper vs. Reel Downloader — A Direct Comparison
| Reel Downloader | Story Sniper | |
|---|---|---|
| How it finds media | getBoundingClientRect() on <video>, checks vertical bounds |
getBoundingClientRect() on <video> + <img>, checks horizontal bounds |
| Target axis | Y-axis (vertical scroll feed) | X-axis (horizontal story navigation) |
| Extraction method | captureStream() + MediaRecorder — records what plays on screen |
React Fiber scraping — reads the original CDN URL from internal props |
| Output format | Re-encoded .webm (VP9 + Opus at 5 Mbps) |
Original .mp4, .jpg, or .webp from Instagram’s CDN |
| Quality ceiling | Limited by screen resolution and MediaRecorder bitrate |
Original upload quality — no re-encoding loss |
| Handles images? | No — video only | Yes — photo Stories are supported |
| Pre-load handling | Ignores videos outside vertical viewport bounds | Ignores media outside horizontal viewport bounds |
| Works when? | Video must play from start to finish before file is ready | Instant — URL is read directly from memory |
The fundamental trade-off is generality vs. precision. The Reel downloader’s captureStream approach is resilient to changes in Instagram’s internal data structures because it never looks at them — it just captures whatever the browser is displaying. The Story Sniper’s fiber-scraping approach is more fragile (React’s internal key names could theoretically change), but it produces the original file without any quality loss.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Alert: “calibrated for /stories/ URL only” | Script run from the wrong page | Navigate to a Story URL (/stories/username/…) first |
| Alert: “No active story detected” | No <video> or <img> element found in viewport |
Make sure the Story is fully visible and has finished loading |
| New tab opens but shows an error | CDN URL has expired | Story CDN links are short-lived; run the script again without navigating away |
| Alert: “Metadata hidden. Use the ‘Delta-Sync’ recorder” | React fiber data not found in the DOM tree | Instagram may have updated their component structure; try the Reel downloader’s captureStream method instead |
| Browser blocks the pop-up | Pop-up blocker intercepted the link.click() |
Allow pop-ups for instagram.com in browser settings |
Using the same browser-only approach, you can also download Reddit videos, download Instagram Reels, save Instagram photos, download YouTube Shorts, and download Twitter/X videos.
Disclaimer: This script is provided for educational purposes only. It demonstrates standard browser and React internals (
getBoundingClientRect, React Fiber,__reactFiber$/__reactProps$properties) that are observable in any browser’s developer tools. Downloading content from Instagram may be against Instagram’s Terms of Service. Only use this on Stories you own, have explicit permission to download, or that are explicitly made available for download. Respect copyright and the work of content creators. Story content may also be subject to additional privacy expectations — exercise discretion.