How Often to Post to Instagram

insights
Author

Julian Winternheimer

Published

July 2, 2025

Overview

This analysis follows up on a previous analysis on posting frequency and follower growth. In this analysis we’ll focus on Instagram, with the aim of trying to find a sweet spot for the amount of posts one should share.

What We Found

The findings in this analysis reveal a clear pattern: posting more frequently to Instagram leads to more follower growth and reach.

Posting at least 1-2 times per week is crucial in order to avoid the no-post penalty, and posting 3-5 times per week is likely a sweet spot for most accounts. It provides substantial growth benefits without requiring an excessive amount of effort.

That being said, bigger gains in follower growth are available for those that are able post more in a sustainable way.The data shows that Instagram increasingly rewards higher posting frequencies, with the jump from 6-9 to 10+ posts per week showing the largest incremental gain.

However, for most accounts, posting 3-5 times per week is probably sufficient. This frequency more than doubles your follower growth compared to posting just once or twice, while still being manageable enough to maintain content quality.

The reach findings are compelling because they show that more frequent posting could result in more reach per post. This is further evidence that Instagram’s algorithm rewards consistent, active accounts by amplifying their content to larger audiences. However, there are diminishing returns in terms of reach per post.

As always, it’s important to remember that sustainable consistency beats unsustainable volume.

Data Collection

The SQL query below returns approximately 2.1 million records that include the number of posts each channel creates per week as well as their weekly follower growth. Approximately 102k profiles are included in this dataset.

Code
sql <- "
with profile_activity_windows as (
  -- Find the first and last week of posting activity for each Instagram profile
  select 
    service_id as channel_id
    , 'instagram' as service
    , min(timestamp_trunc(created_at, week)) as first_post_week
    , max(timestamp_trunc(created_at, week)) as last_post_week
  from dbt_buffer.analyze_instagram_analytics_user_media_totals
  where created_at >= timestamp_sub(current_timestamp, interval 365 day)
  group by 1, 2
),

weekly_instagram_posts as (
  select 
    service_id as channel_id
    , 'instagram' as service
    , timestamp_trunc(created_at, week) as week
    , count(distinct service_update_id) as posts
    , sum(impressions) as total_impressions
    , sum(reach) as total_reach
    , sum(likes) as total_likes
    , sum(comments) as total_comments
    , sum(shares) as total_shares
    , sum(comments + likes + shares) as total_engagements
    , sum(plays) as total_plays
  from dbt_buffer.analyze_instagram_analytics_user_media_totals
  where created_at >= timestamp_sub(current_timestamp, interval 365 day)
  group by 1,2,3
),

weekly_instagram_followers_base as (
  select 
    service_id as channel_id
    , 'instagram' as service
    , timestamp_trunc(checked_at, week) as week
    , max(followers_count) as week_end_followers
  from dbt_buffer.analyze_instagram_analytics_user_daily_totals
  where checked_at >= timestamp_sub(current_timestamp, interval 365 day)
  group by 1,2,3
),

weekly_instagram_followers as (
  select 
    channel_id
    , service
    , week
    , week_end_followers
    , lag(week_end_followers) over (partition by channel_id order by week) as prev_week_followers
  from weekly_instagram_followers_base
),

combined_data as (
  select 
    f.channel_id
    , f.service
    , f.week
    , coalesce(p.posts, 0) as posts
    , f.week_end_followers
    , f.prev_week_followers
    , case 
        when f.prev_week_followers > 0 
        then ((f.week_end_followers - f.prev_week_followers) * 100.0 /
          f.prev_week_followers)
        else null 
      end as follower_growth_pct
    , f.week_end_followers - f.prev_week_followers as follower_growth_absolute
    , coalesce(p.total_impressions, 0) as total_impressions
    , coalesce(p.total_reach, 0) as total_reach
    , coalesce(p.total_likes, 0) as total_likes
    , coalesce(p.total_comments, 0) as total_comments
    , coalesce(p.total_shares, 0) as total_shares
    , coalesce(p.total_engagements, 0) as total_engagements
    , coalesce(p.total_plays, 0) as total_plays
  from weekly_instagram_followers f
  left join weekly_instagram_posts p on f.channel_id = p.channel_id and f.week = p.week
  -- Only include weeks within the active posting window
  inner join profile_activity_windows paw 
    on f.channel_id = paw.channel_id 
    and f.service = paw.service
    and f.week >= paw.first_post_week 
    and f.week <= paw.last_post_week
)

select 
  service
  , channel_id
  , week
  , posts
  , week_end_followers
  , prev_week_followers
  , follower_growth_pct
  , follower_growth_absolute
  , total_impressions
  , total_reach
  , total_likes
  , total_comments
  , total_shares
  , total_engagements
  , total_plays
  , case 
      when posts = 0 then 'No Posts'
      when posts between 1 and 2 then '1-2 Posts'
      when posts between 3 and 5 then '3-5 Posts'
      when posts between 6 and 10 then '6-10 Posts'
      when posts > 10 then '10+ Posts'
    end as posting_frequency_bin
from combined_data
where prev_week_followers is not null
order by channel_id, week
"

# get data from BigQuery
posts <- bq_query(sql = sql)

We’ll group the number of posts sent in a given week into five separate buckets.

Code
# remove NA values and update posting bins
posts <- na.omit(posts) %>% 
  mutate(posting_frequency_bin = case_when(
    posts == 0 ~ 'No Posts',
    posts %in% 1:2 ~ '1-2 Posts',
    posts %in% 3:5 ~ '3-5 Posts',
    posts %in% 6:9 ~ '6-9 Posts',
    posts >= 10 ~ '10+ Posts'),
    posting_frequency_bin = factor(posting_frequency_bin,
                                   levels = c("No Posts",
                                              "1-2 Posts",
                                              "3-5 Posts",
                                              "6-9 Posts",
                                              "10+ Posts")))

Next we’ll calculate the average follower growth rate by the number of posts sent in a given week. The plot below shows a strong positive correlation, which is what we might expect.

Code
# summary stats
posts %>% 
  group_by(posting_frequency_bin) %>% 
  summarise(avg_growth = mean(follower_growth_pct, na.rm = T)) %>% 
  ggplot(aes(x = posting_frequency_bin, y = avg_growth)) +
  geom_col(show.legend = FALSE) +
  labs(x = "Weekly Posts Shared",
       y = NULL,
       title = "Average Follower Growth Rate by Posting Frequency",
       subtitle = "Instagram Profiles Only")

This relationship is nice to see, but it may not tell us the full story. We need to control for differences in accounts’ natural growth rates. As an example, imagine that the fastest growing accounts naturally post more frequently than accounts with more modest growth.

For example, imagine a fast growth account that gains 1,000 followers per week and posts 20 times, while a smaller gains 10 followers per week and only posts twice. If we simply average these together, we might conclude that posting 20 times leads to fast growth. However, the larger account would likely gain a higher number of followers even if they posted less frequently.

Without accounting for these inherent differences in accounts, we can’t tell whether frequent posting causes growth, or whether naturally fast-growing accounts just happen to post more. That’s why we need to employ statistical methods that compare each account against itself, essentially asking the question “when this specific account posts more versus less, how does their growth change?”

How We Account for Inherent Account Differences

We use two complementary approaches to ensure you’re measuring the true effect of posting frequency:

Z-Score Analysis Instead of comparing different channels to each other, which could result in unfair comparisons because some channels naturally grow faster, we compare each channel to its own typical performance. The Z-score tells us h much better or worse did a channel performed compared to its own average.

For example, if Channel A typically grows at 2% per week and Channel B typically grows at 0.5% per week, a 1% growth week would be below average for Channel A, represented by a negative Z-score, and above average for channel B, represented by a positive Z-score.

Fixed Effects Regression This statistical technique takes the control even further. It asks the question “when the same channel posts more in some weeks and less in others, how does that affect their growth?”

This method essentially compares each channel’s high posting weeks to their own low-posting weeks. This should remove any confusion about whether successful accounts just happen to post more because we calculate the effect within the same account.

Z-Score Analysis

This is the approach to calculate a Z-score, which we’ll do for each channel:

  1. Calculate the mean and standard deviation of follower growth rates across all weeks
  2. Calculate a Z-score for each week. Z = (follower growth - average growth) ÷ standard deviation.

For each channel and week, the Z-score tells us how many standard deviations above or below average a channel performed in a given week:

Z = 0: Typical performance for this channel Z = +1: Strong positive week (better than ~84% of weeks) Z = +2: Exceptional week (better than ~97% of weeks) Z = -1: Poor week (worse than ~84% of weeks)

Once we’ve calculated Z-scores for each channel, we average them for all channels.

Code
# calculate channel-specific mean and standard deviation
channel_stats <- posts %>%
  group_by(channel_id) %>%
  summarise(
    mean_growth = mean(follower_growth_pct, na.rm = TRUE),
    sd_growth = sd(follower_growth_pct, na.rm = TRUE),
    n_weeks = n()
  ) %>%
  # only keep channels with sufficient data and variation
  filter(n_weeks >= 3, sd_growth > 0)

# merge back and calculate Z-scores
posts_with_z <- posts %>%
  inner_join(channel_stats, by = "channel_id") %>%
  mutate(z_score = (follower_growth_pct - mean_growth) / sd_growth)

# calculate summary statistics
posts_with_z %>%
  group_by(posting_frequency_bin) %>%
  summarise(
    n_observations = n(),
    mean_z_score = mean(z_score, na.rm = TRUE),
    median_z_score = median(z_score, na.rm = TRUE)
  )
# A tibble: 5 × 4
  posting_frequency_bin n_observations mean_z_score median_z_score
  <fct>                          <int>        <dbl>          <dbl>
1 No Posts                      597068     -0.0790         -0.236 
2 1-2 Posts                     726617      0.00337        -0.188 
3 3-5 Posts                     348269      0.0661         -0.143 
4 6-9 Posts                     106584      0.111          -0.115 
5 10+ Posts                      71424      0.138          -0.0785

This data suggests that there is a strong positive relationship between posting frequency and follower growth on Instagram. Channels that post more frequently consistently gain more followers relative to their baseline.

It also suggests that there is a cost to not posting. Channels that don’t post at all significantly under-perform their baseline growth rates. Even posting once or twice a week results in a significant increase in follower growth compared to weeks with no posts.

Fixed Effects Regression Model

A fixed effects regression model compares the same channel against itself over time, rather than comparing different channels to each other.

The model essentially creates a separate baseline for each channel, then measures how posting frequency affects growth relative to that channel’s own average performance.

The key advantage is that we can make stronger causal claims about posting frequency. When we see that the same channel grows faster during weeks when it posts more, we can be more confident that posting is actually driving the growth rather than just being correlated with it.

Code
# fit fixed effects model
fe_model <- feols(follower_growth_pct ~ posting_frequency_bin | channel_id, 
                   data = posts, 
                   cluster = "channel_id")
# summarise model
summary(fe_model)
OLS estimation, Dep. Var.: follower_growth_pct
Observations: 1,880,815
Fixed-effects: channel_id: 84,848
Standard-errors: Clustered (channel_id) 
                               Estimate Std. Error t value  Pr(>|t|)    
posting_frequency_bin1-2 Posts 0.118662   0.004030 29.4479 < 2.2e-16 ***
posting_frequency_bin3-5 Posts 0.259663   0.006311 41.1429 < 2.2e-16 ***
posting_frequency_bin6-9 Posts 0.437659   0.011808 37.0660 < 2.2e-16 ***
posting_frequency_bin10+ Posts 0.662456   0.022889 28.9426 < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
RMSE: 1.79095     Adj. R2: 0.337408
                Within R2: 0.003413

The fixed effects model confirms a strong, statistically significant relationship between posting frequency and follower growth on Instagram. All posting frequency categories show positive and highly significant effects compared to weeks with no posts.

This effect grows progressively larger with more frequent posting. Posting 3-5 times per week yields a 0.26 percentage point increase from 1-2 posts, 6-9 posts delivers more gains, and 10+ posts provides the largest follower boost of all.

The increasing incremental gains suggest that Instagram’s algorithm could reward highly active accounts. However, it’s worth mentioning that the effort required to maintain 10 or more posts per week could be substantial.

For most accounts, the 3-5 post range likely represents the sweet spot where meaningful growth benefits can be achieved with manageable effort.

Either way, these results provide pretty clear evidence that consistent, frequent posting is a major driver of follower growth.

Posting Frequency and Reach

Next we’ll look at how posting frequency affects reach. We’ll use a similar approach to what we did with follower growth.

Code
# calculate median reach by posting frequency bin
posts %>%
  group_by(posting_frequency_bin) %>%
  summarise(
    n_observations = n(),
    avg_total_reach = mean(total_reach, na.rm = TRUE),
    median_total_reach = median(total_reach, na.rm = TRUE),
    avg_reach_per_post = mean(total_reach / pmax(posts, 1), na.rm = TRUE)
  ) %>%
  ggplot(aes(x = posting_frequency_bin, y = median_total_reach)) +
  geom_col() +
  scale_y_continuous(labels = comma) +
  labs(
    x = "Weekly Posts Shared",
    y = "Median Total Reach",
    title = "Total Weekly Reach by Posting Frequency",
    subtitle = "More posts generally lead to higher total reach"
  )

Naturally, accounts that post more get more total reach. Next, let’s looks at reach per post.

Code
# calculate median reach by posting frequency bin
posts %>%
  group_by(posting_frequency_bin) %>%
  summarise(
    n_observations = n(),
    avg_total_reach = mean(total_reach, na.rm = TRUE),
    median_total_reach = median(total_reach, na.rm = TRUE),
    avg_reach_per_post = mean(total_reach / pmax(posts, 1), na.rm = TRUE),
    med_reach_per_post = median(total_reach / pmax(posts, 1), na.rm = TRUE)
  ) %>%
  ggplot(aes(x = posting_frequency_bin, y = med_reach_per_post)) +
  geom_col() +
  scale_y_continuous(labels = comma) +
  labs(
    x = "Weekly Posts Shared",
    y = "Median Reach Per Post",
    title = "Reach Per Post by Posting Frequency",
    subtitle = "Accounts that post more tend to get more reach per post"
  )

Now let’s apply the same fixed effects approach to control for channel differences.

Code
# create a log-transformed version of reach for the regression
posts_reach <- posts %>% 
  mutate(reach_per_post = log(total_reach / pmax(posts, 1) + 1))

# fit fixed effects model for total reach
fe_model_reach <- feols(reach_per_post ~ posting_frequency_bin | channel_id, 
                        data = posts_reach, 
                        cluster = "channel_id")

# summary of the model
summary(fe_model_reach)
OLS estimation, Dep. Var.: reach_per_post
Observations: 1,880,815
Fixed-effects: channel_id: 84,848
Standard-errors: Clustered (channel_id) 
                               Estimate Std. Error t value  Pr(>|t|)    
posting_frequency_bin1-2 Posts  3.76999   0.007917 476.191 < 2.2e-16 ***
posting_frequency_bin3-5 Posts  4.21026   0.008572 491.144 < 2.2e-16 ***
posting_frequency_bin6-9 Posts  4.44137   0.010121 438.829 < 2.2e-16 ***
posting_frequency_bin10+ Posts  4.65568   0.014612 318.625 < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
RMSE: 1.05895     Adj. R2: 0.820263
                Within R2: 0.679177

Each step up in posting frequency delivers substantial percentage gains in reach per post, with the largest jump occurring when moving from 1-2 to 3-5 posts. Even at higher frequencies, there are still meaningful 20-25% incremental gains for posting more frequently, however the returns are diminishing.

  • Moving from 1-2 to 3-5 posts yields an additional ~56% gain in reach per post
  • Moving from 3-5 to 6-9 posts provides another ~26% gain
  • Moving from 6-9 to 10+ posts delivers an additional ~24% gain