

# How to Save Instagram Metric Snapshots Without Duplicate Imports

Store each collected Instagram snapshot once, using a stable key for its source run and row. Keep the original collection time. Add later observations as new rows, and build a separate view for the latest readings. When an import fails, restart with the same keys. A repeated import must add no new observations. A new collection run must add new observations, even when the counts stay the same.

A snapshot is a saved reading at one collection time. An observation is the stored record of that reading. This guide proposes an import method for developers who already collect public Instagram metrics. The method keeps history and prevents a repeated import from becoming a false extra check.

The collection program supplies source rows. Your import process must supply the stable keys, storage rules, and restart checks described below. No collection run or import test was performed for this article.

## Define what each saved row means

A public reader asked about “Analysing the post performance every 24 hour to create a curve similar to the new Instagram kpis.” That wording identifies a need for repeated measurements. It does not specify a retry design. The safe import method here is a proposed design extension. [Reader question, observed August 12, 2026; publication date unavailable and no verified product use](https://www.reddit.com/r/n8n/comments/1iy4oco/n8n_competitor_analysis/).

A daily schedule alone does not produce reliable history. You also need to distinguish two events:

- A new collection: The program checks the post again. Save a new observation.
- A repeated import: Your app reads a row it already saved. Keep the existing observation.

These events can contain identical counts. Two checks that both report 100 views are still two observations. Reading one collected row twice is still one observation.

Keep counts separate from people. A public view count is not a count of unique people. Keep `views`, `plays`, and `playsInstagram` as separate fields. Do not add them together or replace one with another without a defined reporting rule.

This method concerns public counts. It does not obtain private account Insights or recover past counts you did not save. Owner-only records require a separate authorized source. The checked public contract supports current public metric collection, not historical count recovery. [Instagram Metrics Scraper contract, checked October 25, 2026; public build inspection only, no paid run](https://apify.com/chronometrica/instagram-metrics-scraper).

If you still need to choose between finding recent posts and checking saved URLs, use the [recurring Instagram collection guide](/en/articles/chronometrica-vs-webdatalabs-recurring-instagram-metrics/). Make that choice before you design the import.

## Keep an observation table and an import receipt

Use two records with different jobs. The observation table holds collected readings. The import receipt records how your app processed a source run.

A run is one execution of the collection program. A dataset is the stored list of rows returned by that program. Keep both identifiers so you can trace a saved observation to its source.

The following names are proposed storage fields. Fields such as `run_id` and `item_position` are your workflow metadata: information about collection and import. They are not new Actor output fields. An Actor is a hosted collection program.

| Saved field | What to keep | Purpose |
| --- | --- | --- |
| `observation_key` | Unique source-row key | Prevent a repeated insert |
| `run_id` | Completed source run ID, as text | Identify the collection run |
| `dataset_id` | Source dataset ID, as text | Identify the source list |
| `item_position` | Original absolute row position | Identify the row before changes |
| `postId` | Original post ID, as text | Match content across runs |
| `postUrl` | Original content URL | Retain the content reference |
| `scrapedAt` | Original collection time | Place the reading in history |
| Counters | Each returned counter separately | Preserve metric meaning |
| Status fields | Original metric and row status | Explain missing or unusable values |
| `original_json` | Complete original row | Retain source evidence |
| `imported_at` | Time your app saved the row | Track import work separately |

JSON is a text format for structured data. Save the original JSON before you rename fields, remove fields, or expand one source row into several records.

Preserve `postMetricStatus`, `status`, and `statusReason` when supplied. Retain other returned status fields in the original JSON. Do not invent status values or assume an undocumented status structure. The checked output contract includes content identity, counts, collection time, and status fields. [Public default build, checked October 25, 2026; documented fields only, no runtime test](https://api.apify.com/v2/acts/Y7Ib4ARpUHw5Ue8op/builds/default).

For each import attempt, save a receipt with these details:

- Run ID, dataset ID, and source completion state.
- Fixed export settings and the source row range being imported.
- Accepted rows, repeated rows, and rejected rows.
- Last durably processed source position.
- Import start time, end time, and completion state.
- Reasons for rejected rows.

“Accepted” means a new observation was saved. “Repeated” means the same source observation was already saved. “Rejected” means the row needs correction or review under your stated rules.

Keep counts for each import attempt separate. Adding the receipts from several retries does not give the number of distinct observations.

## Choose the observation key before loading rows

A key is a value that identifies one record. Your observation key must identify the source row, not just the Instagram post.

Using only `postId` would merge all later checks of that post. Using import time would create a new observation whenever you retry. A caption or position in a creator’s profile list also cannot identify a collected observation.

For a stable completed dataset, use the run ID and original dataset item position. Keep the dataset ID with that key. A practical storage rule makes the combination of run ID, dataset ID, and item position unique.

A unique rule prevents the table from accepting two records with that same combination. Apply it in storage, rather than relying only on a check in application code before insertion.

Follow these steps:

1. Select a completed source run when your reporting rule requires stable completed data.
2. Save its run ID and dataset ID.
3. Fix the raw export settings and ascending source order.
4. Record the source range you intend to import.
5. Assign absolute original positions before sorting, filtering, or expanding rows.
6. Keep those positions unchanged on every retry.

Absolute position means position in the full source list. If one page contains positions 0 through 99, the next starts at 100. It does not restart at 0. State whether your positions start at 0 or 1.

Apify documents dataset storage as append-only: stored items can be added, but existing items cannot be modified or deleted. This supports retaining source rows. It does not make a filtered or reordered export a safe import identity by itself. [Apify dataset documentation, checked October 25, 2026; append-only storage and retrieval facts, not an import guarantee](https://docs.apify.com/storage/dataset).

If you cannot retain stable source positions, use a documented hash of a fixed original row representation. A hash is a calculated identifier made from content. Use the same fields, text representation, and calculation on every retry. Keep the run and dataset references too.

This alternative needs care. Identical rows can produce the same hash, even when they occupied different source positions. Define whether those rows represent one observation or two before using this method. Prefer stable source positions when you must preserve each returned row.

If an existing key points to different original content, stop and review the conflict. Do not silently overwrite the saved observation or call it a normal repeat.

## Save rows and progress as one operation

The main restart risk occurs between saving a row and saving progress. Your app can stop after one succeeds but before the other does.

A transaction saves related changes as one operation. Where your storage tool supports transactions, commit the accepted observations and their progress position together. Keep the related receipt changes consistent with that commit.

Use this import sequence:

1. Read the next source rows with their original positions.
2. Check each row against your acceptance rules.
3. Build its observation key.
4. Insert a new observation when the key is absent.
5. Count a matching existing observation as a repeat.
6. Record rejected rows and their reasons.
7. Save progress only when the rows and rejection records are durable.

Durable means the storage system has saved the change so it remains after the import process stops.

A missing counter alone should not reject an otherwise valid source row. The row can still record a collection attempt and other available counts. Preserve the missing value and its status.

Keep enough source information to review rejected rows. A progress position beyond a rejected row must not hide that row. Store its rejection record before advancing progress, or stop there until you resolve it.

If your storage tool cannot commit rows and progress together, save rows before advancing progress. After a stop, replay any uncertain range through the same unique key. This can produce repeats, but it must not produce extra observations.

Do not create a fresh `scrapedAt` during replay. `imported_at` can change between attempts. The collection time cannot.

## Work through one import and a restart

The following example is original fiction. It is not Actor output, a customer result, or a test result. All times refer to the same fictional day in UTC, a common time standard.

Run A contains two rows. For this example, source positions start at 0.

| Run | Original position | Post | Collection time | Views |
| --- | --- | --- | --- | --- |
| A | 0 | P1 | 10:00 UTC | 100 |
| A | 1 | P2 | 10:01 UTC | Missing |
| B | 0 | P1 | 11:00 UTC | 120 |

First, import run A. The table saves P1 and P2 as two observations. P2 has no available view count. It remains a saved observation with missing views.

Now replay run A. Both source-row keys already exist. The import adds no observations and reports two repeats.

Next, import run B. Its P1 row has a different run identity. Save it as a new observation, even though P1 already exists in the table.

| Import attempt | New observations | Repeated rows | Total saved observations |
| --- | --- | --- | --- |
| First import of A | 2 | 0 | 2 |
| Replay of A | 0 | 2 | 2 |
| First import of B | 1 | 0 | 3 |

The arithmetic is:

- First import: 0 observations + 2 new observations = 2 observations.
- Replay: 2 observations + 0 new observations = 2 observations.
- New run: 2 observations + 1 new observation = 3 observations.

There are three observations from two runs. There are five row-processing events across the three attempts, but those events do not represent five observations. P2 remains missing. It does not become zero.

Now consider a separate restart branch of this same original fiction. The first import of A stops after saving P1. It has not saved P2.

If the saved progress is reliable, restart at P2. That restart adds one observation. The table finishes with two observations from A.

If progress is uncertain, replay A from the start. P1 becomes one repeat, and P2 becomes one new observation. The table still finishes with two observations from A.

After importing B, either restart path produces the same three saved observations. This is the result your retry check should verify.

## Build the latest display without changing history

A view is a result assembled from saved records. Build a latest-reading view instead of replacing old observations.

Order valid observation times by `scrapedAt`, not by import time. Importing yesterday’s completed run today must not make yesterday’s count appear newer than a reading collected today.

Show two facts separately for each post:

- The latest saved collection attempt.
- The latest usable reading for each required counter, with that reading’s original time.

A newer row can have missing views while an older row has usable views. You can retain the older value for reference, but show its older time and the newer missing result. Do not display the older value as a fresh measurement.

“Latest usable” can differ by counter. The latest row with usable likes may differ from the latest row with usable views. Keep their times separate.

Document these ordering cases before release:

- Equal collection times: Keep both source observations. Show the tie or apply a stated display rule. A display rule does not prove which reading occurred later.
- Missing collection time: Preserve the source row, but exclude it from time-based ordering until reviewed.
- Invalid collection time: Record the problem. Do not substitute import time without a clear label.
- No returned row: Record the collection gap separately. Do not invent an observation or a zero count.

For later calculations, use the [monthly Instagram reporting guide](/en/articles/instagram-publication-cohort-view-contribution/). This import method preserves observations; it does not establish missing month-boundary readings. The [metric history guide](/en/articles/facebook-post-metrics-history/) also explains why actual observation times matter when checks have gaps.

## Use a small collection input, then check the import

Chronometrica’s Instagram Metrics Scraper documents direct content inputs and the fields needed for this proposed storage method. Its documented fit does not mean it builds your history table or enforces your import keys.

A Task is a saved input preset for an Actor. The public Instagram Post Metrics Tracker Task provides a starting point. The supplied page check confirmed public presence only. It did not verify authenticated saved settings or run the preset. [Instagram Post Metrics Tracker, checked October 25, 2026; public page presence only, no authenticated input check or paid run](https://apify.com/chronometrica/instagram-metrics-scraper/examples/instagram-post-metrics-tracker).

Use this supplied small input:

```json
{"maxItems":1,"mode":"contentUrls","startUrls":["https://www.instagram.com/reel/DIwcCZgxVZP/"]}
```

Use a small collection input, then check the import

`mode` selects how the Actor handles inputs. `contentUrls` selects direct content checks. `startUrls` contains the intended content URLs. `maxItems` limits the number of returned metric rows.

Replace the sample URL with one known public post or Reel URL. A Reel is an Instagram video format. Replace the sample list rather than adding your URL beside it. Inspect the saved settings before use.

This input passed a static schema check. That check confirms the input form against the supplied public schema. It does not confirm live content availability or successful collection. [Public input schema, checked October 25, 2026; static validation only, no paid run](https://api.apify.com/v2/acts/Y7Ib4ARpUHw5Ue8op/builds/default).

The following checks are proposed reader actions:

1. Collect one intended public item and retain the completed run and dataset references.
2. Import its rows and record accepted, repeated, and rejected counts.
3. Import the same source again. Confirm zero new observations.
4. Stop an import partway through, then restart it. Confirm the final source-row keys match a complete import.
5. Collect a new run for the same item. Confirm it creates a new observation.
6. Confirm all saved `scrapedAt` values match their source rows.
7. Confirm missing counters remain missing and older usable counts retain their original times.

A completed run does not prove every requested item has a usable counter. Compare the intended content list with the returned rows and record gaps separately.

## Make the next import safe

Start with the smallest complete check: one completed source dataset, one observation table, and one receipt. Import it twice. Proceed to repeated collection only when the second import adds zero observations and a later run adds a new observation.

Use the [Instagram Metrics Scraper, contract checked October 25, 2026; documented public collection fit, no runtime test for this guide](https://apify.com/chronometrica/instagram-metrics-scraper) to collect the public rows your app needs. Keep collection, import, and display times distinct. If the retry check fails, correct the observation key or progress handling before adding more scheduled data.

## Sources

- [Instagram Metrics Scraper public contract, checked October 25, 2026; documented public inputs and outputs, no paid run or current runtime health test](https://apify.com/chronometrica/instagram-metrics-scraper)
- [Public default build 1.0.35, checked October 25, 2026; schema and documented field evidence, not measured runtime behavior](https://api.apify.com/v2/acts/Y7Ib4ARpUHw5Ue8op/builds/default)
- [Apify dataset documentation, checked October 25, 2026; append-only storage and retrieval evidence, stable import identity still requires fixed source order](https://docs.apify.com/storage/dataset)
- [Instagram Post Metrics Tracker Task, checked October 25, 2026; public preset presence only, saved settings not authenticated and no paid run](https://apify.com/chronometrica/instagram-metrics-scraper/examples/instagram-post-metrics-tracker)
- [Recurring measurement question, observed August 12, 2026; publication date unavailable, one public question rather than evidence of demand or product use](https://www.reddit.com/r/n8n/comments/1iy4oco/n8n_competitor_analysis/)