ds@mcpanalytics.aiEmail your data and your question and get an answer about your own data, with the method shown.Email your data and question. Get an answer.

Shopify Order Timing: When Do Customers Buy?

By MCP Analytics Team | Published , updated

Learn order timing patterns and use them to plan marketing tests

Introduction to Order Timing Analysis

Understanding when your customers are most likely to place orders is a basic question in e-commerce analytics. Order timing analysis shows the days and hours when your Shopify store receives the most orders, enabling you to strategically align your marketing efforts with customer behavior patterns.

When you know that your customers primarily order on Tuesday evenings or Sunday mornings, you can schedule email campaigns to arrive just before these peak windows, launch flash sales during busy order hours, and decide where to test advertising schedules. Order counts show when orders arrive, not when visitors are most likely to buy: a conversion rate needs visits as well as orders, which this tutorial does not use. Treat the timing pattern as a starting point for tests, not as proof of what works.

In this comprehensive tutorial, you'll learn how to conduct a thorough order timing analysis for your Shopify store, interpret the results, and implement actionable strategies based on your findings.

Prerequisites and Data Requirements

Before beginning your order timing analysis, ensure you have the following in place:

Required Access and Data

Recommended Tools

What You'll Accomplish

By completing this tutorial, you will:

Step 1: What Day of the Week Has the Most Orders?

The first step in order timing analysis is identifying which days of the week drive the most orders. This shapes your marketing calendar and shows your customers' weekly rhythm.

Last updated 2026-11-02. All outputs on this page come from running the code shown against a synthetic (made-up) data set of 1,177 orders over 12 weeks (2026-07-06 to 2026-09-27). It is not a real store, and your numbers will differ. The data come from the seeded generator below (seed 2026, run with Python 3 and numpy and pandas). I could not test the fetch code against a live store; it follows the shape of Shopify's Admin GraphQL reference, so check it against the current docs.

The Synthetic Data Generator

Run this first. It makes the orders table and shop_tz used by every output on this page, with ISO 8601 UTC timestamps like the API returns. The day and hour weights, the order values and the one-hour weekend shift are all made up. If you use your real store, skip this and use fetch_orders below.

import numpy as np
import pandas as pd

# MADE-UP DATA: a seeded generator, not a real store.
rng = np.random.default_rng(2026)
shop_tz = "America/Chicago"

days = pd.date_range("2026-07-06", "2026-09-27", freq="D")       # 12 weeks, Monday to Sunday
day_mean = [13.5, 17.0, 18.0, 18.0, 15.0, 7.5, 7.5]               # made-up mean orders per day, Monday first
hour_w = np.array([1, 1, 1, 1, 1, 2, 4, 6, 6, 8, 11, 11,
                   11, 9, 9, 10, 7, 7, 5, 5, 5, 4, 2, 2], dtype=float)
hour_w /= hour_w.sum()                                            # made-up share of orders by local hour

rows = []
for d in days:
    weekend = d.dayofweek >= 5
    n = rng.poisson(day_mean[d.dayofweek])
    p = np.roll(hour_w, 1) if weekend else hour_w                 # weekend hours shifted one hour later
    hours = rng.choice(24, size=n, p=p)
    secs = rng.integers(0, 3600, size=n)
    amounts = np.round(rng.gamma(4.0, (105 if weekend else 90) / 4.0, size=n), 2)
    for h, s, a in zip(hours, secs, amounts):
        local = d.tz_localize(shop_tz) + pd.Timedelta(hours=int(h), seconds=int(s))
        rows.append({"created_at": local.tz_convert("UTC").strftime("%Y-%m-%dT%H:%M:%SZ"),
                     "amount": float(a)})

orders = pd.DataFrame(rows).sort_values("created_at", ignore_index=True)
orders.insert(0, "id", [f"gid://shopify/Order/{1001 + i}" for i in range(len(orders))])
print(f"Made-up orders: {len(orders)}")
Made-up orders: 1177

Accessing Your Order Data

Read the orders with the GraphQL Admin API (the endpoint and X-Shopify-Access-Token header are in Shopify's reference). The query is paginated with a cursor and filters on created_at. Each order's total comes from currentTotalPriceSet, the price after returns including taxes and discounts. This block needs a real store and an access token; skip it when you use the made-up data above (it was not run for this page).

import os
import requests
import pandas as pd

SHOP = "your-store"                              # the part before .myshopify.com
API_VERSION = "2026-10"                          # use a version Shopify lists as supported
TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]       # needs read_orders; older than 60 days also needs read_all_orders

QUERY = """
query OrderTimes($pageSize: Int!, $cursor: String, $filter: String) {
  shop { ianaTimezone }
  orders(first: $pageSize, after: $cursor, query: $filter) {
    pageInfo { hasNextPage endCursor }
    nodes {
      id
      createdAt
      currentTotalPriceSet { shopMoney { amount currencyCode } }
    }
  }
}
"""

def fetch_orders(since, page_size=250):
    url = f"https://{SHOP}.myshopify.com/admin/api/{API_VERSION}/graphql.json"
    headers = {"X-Shopify-Access-Token": TOKEN}
    cursor, rows, tz = None, [], None
    while True:
        variables = {"pageSize": page_size, "cursor": cursor, "filter": f"created_at:>={since}"}
        resp = requests.post(url, json={"query": QUERY, "variables": variables}, headers=headers, timeout=30)
        resp.raise_for_status()
        body = resp.json()
        if "errors" in body:
            raise RuntimeError(body["errors"])
        tz = body["data"]["shop"]["ianaTimezone"]
        page = body["data"]["orders"]
        rows += [{"id": o["id"], "created_at": o["createdAt"],
                  "amount": float(o["currentTotalPriceSet"]["shopMoney"]["amount"])} for o in page["nodes"]]
        if not page["pageInfo"]["hasNextPage"]:
            return pd.DataFrame(rows), tz
        cursor = page["pageInfo"]["endCursor"]

# Real store only. With the made-up data above, do not run this line.
# orders, shop_tz = fetch_orders(since="2026-07-06", page_size=250)

Preparing the Timestamps

Whichever way you got orders and shop_tz, convert the timestamps to the shop's time zone and take the weekday and hour:

# Parse the ISO 8601 timestamps as absolute times, then express them in the SHOP's time zone.
orders["local"] = pd.to_datetime(orders["created_at"], utc=True).dt.tz_convert(shop_tz)
orders["dow"] = orders["local"].dt.dayofweek          # 0 = Monday
orders["hour"] = orders["local"].dt.hour

print(f"Orders: {len(orders)}   Shop time zone: {shop_tz}")
print(f"First: {orders['local'].min():%Y-%m-%d %H:%M}   Last: {orders['local'].max():%Y-%m-%d %H:%M}")
Orders: 1177   Shop time zone: America/Chicago
First: 2026-07-06 08:45   Last: 2026-09-27 19:53

Analyzing Day-of-Week Patterns

Count orders by weekday, and check that the peak day holds in both halves of the period:

from scipy import stats

names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
by_day = orders["dow"].value_counts().reindex(range(7), fill_value=0)

day_table = pd.DataFrame({"Day": names, "Orders": by_day.values,
                          "Share of orders": (by_day / by_day.sum() * 100).round(1).astype(str).values + "%"})
print(day_table.to_string(index=False))
print(f"Total orders: {by_day.sum()}   Peak day: {names[by_day.idxmax()]}")

weekend = by_day[[5, 6]].sum()
print(f"Weekdays: {by_day.sum() - weekend} ({(by_day.sum() - weekend) / by_day.sum():.1%})   Weekend: {weekend} ({weekend / by_day.sum():.1%})")

# Does the peak day hold in both halves of the period?
mid = orders["local"].min() + (orders["local"].max() - orders["local"].min()) / 2
for label, part in [("first half", orders[orders["local"] <= mid]), ("second half", orders[orders["local"] > mid])]:
    c = part["dow"].value_counts()
    print(f"Peak day, {label}: {names[c.idxmax()]} ({c.max()} of {len(part)} orders)")

# Chi-square test against equal shares for the 7 days.
chi2, p = stats.chisquare(by_day.values)
print(f"Chi-square vs equal shares: chi2 = {chi2:.1f}, p = {p:.2g}")

Expected Output

      Day  Orders Share of orders
   Monday     158           13.4%
  Tuesday     203           17.2%
Wednesday     222           18.9%
 Thursday     214           18.2%
   Friday     193           16.4%
 Saturday     102            8.7%
   Sunday      85            7.2%
Total orders: 1177   Peak day: Wednesday
Weekdays: 990 (84.1%)   Weekend: 187 (15.9%)
Peak day, first half: Wednesday (112 of 587 orders)
Peak day, second half: Wednesday (110 of 590 orders)
Chi-square vs equal shares: chi2 = 108.4, p = 4.4e-21

Interpreting Your Results

In this example, Wednesday has the most orders, 222 of 1,177 (18.9%), with Thursday at 214 and Tuesday at 203. Wednesday is the peak day in both halves of the period (112 of 587 orders, then 110 of 590), but Thursday is close, so treat Tuesday to Thursday as the busy days rather than relying on one. Weekdays hold 84.1% of orders and the weekend 15.9%.

The chi-square test (Pearson, 1900) compares the seven counts with equal shares across the days. The p-value is the probability of data at least this extreme if there were no real difference between days; a small p-value is evidence against no difference, not proof of an effect, and a large one is not proof of no effect. Here it is very small, so equal shares across days fit these data poorly. It does not say why the days differ, and orders from one customer are not independent of each other, so treat the p-value as a rough guide.

Key Questions to Ask

Step 2: What Time of Day Do Customers Typically Order?

Hourly patterns show when your customers place orders. This is useful when timing email sends, promotions and social posts.

Viewing Hourly Order Distribution

Group the orders by the hour in the shop's time zone, regardless of the day. The shares below are computed from the same total as the counts, so the table and the summary lines agree:

by_hour = orders["hour"].value_counts().reindex(range(24), fill_value=0)
share = by_hour / by_hour.sum() * 100

hour_table = pd.DataFrame({
    "Hour": [f"{h:02d}:00-{(h + 1) % 24:02d}:00" for h in range(24)],
    "Orders": by_hour.values,
    "Share": share.round(1).astype(str).values + "%",
})
print(hour_table.to_string(index=False))

peak = by_hour.idxmax()
window = by_hour.loc[9:15].sum()                 # 09:00 up to 16:00 = hours 9 to 15
top3 = by_hour.nlargest(3)
print()
print(f"Total orders: {by_hour.sum()}")
print(f"Peak hour: {peak:02d}:00-{peak + 1:02d}:00 ({share[peak]:.1f}% of orders)")
print(f"09:00-16:00 window: {window} orders ({window / by_hour.sum():.1%})")
print(f"Top 3 hours: {sorted(top3.index.tolist())} = {top3.sum() / by_hour.sum():.1%} of orders (flat would be {3 / 24:.1%})")
       Hour  Orders Share
00:00-01:00       8  0.7%
01:00-02:00       7  0.6%
02:00-03:00      12  1.0%
03:00-04:00       6  0.5%
04:00-05:00       8  0.7%
05:00-06:00      14  1.2%
06:00-07:00      35  3.0%
07:00-08:00      55  4.7%
08:00-09:00      50  4.2%
09:00-10:00      63  5.4%
10:00-11:00      91  7.7%
11:00-12:00      92  7.8%
12:00-13:00     112  9.5%
13:00-14:00      84  7.1%
14:00-15:00      89  7.6%
15:00-16:00      93  7.9%
16:00-17:00      81  6.9%
17:00-18:00      64  5.4%
18:00-19:00      52  4.4%
19:00-20:00      39  3.3%
20:00-21:00      47  4.0%
21:00-22:00      35  3.0%
22:00-23:00      24  2.0%
23:00-00:00      16  1.4%

Total orders: 1177
Peak hour: 12:00-13:00 (9.5% of orders)
09:00-16:00 window: 624 orders (53.0%)
Top 3 hours: [11, 12, 15] = 25.2% of orders (flat would be 12.5%)

Identifying Your Busy Hours

What the output says for this example:

How Sure Can You Be?

Hourly counts are small numbers, so a gap of a few orders between two hours is within the normal variation. Prefer windows to exact times. A peak that is only one or two orders ahead of its neighbors is not a peak you should plan around. For a formal comparison of two versions of a campaign, see our guide on A/B testing statistical significance.

Day and Hour Together

The peak day and the peak hour are measured separately above. Counting day-by-hour cells shows whether they coincide:

# Day-by-hour cells: the peak day and the peak hour are not necessarily peak together.
cells = orders.groupby(["dow", "hour"]).size().rename("Orders").reset_index()
cells["Day"] = cells["dow"].map(dict(enumerate(names)))
cells["Hour"] = cells["hour"].map(lambda h: f"{h:02d}:00")
top = cells.sort_values(["Orders", "dow", "hour"], ascending=[False, True, True]).head(5)
print(top[["Day", "Hour", "Orders"]].to_string(index=False))
print(f"Cells with at least one order: {len(cells)} of 168")
      Day  Hour  Orders
  Tuesday 12:00      27
Wednesday 12:00      22
 Thursday 14:00      22
   Friday 12:00      22
   Monday 11:00      21
Cells with at least one order: 158 of 168

Here the busiest single cell is Tuesday 12:00 (27 orders), and Tuesday is not the peak day. Wednesday 12:00, the peak day at the peak hour, has 22, tied with two other cells. The top cells are within a few orders of each other, so a single best day-and-hour slot does not follow from the two separate tables; it needs the day-by-hour counts, and even those are thin.

Step 3: When Should I Schedule Marketing Campaigns?

Now translate the pattern into a test plan. Order counts tell you when orders arrive, not when a message works, so each item below is something to test.

Candidate Windows From This Example

The Pre-Peak Idea

One common idea is to send a message shortly before the hours when orders cluster, so that it is fresh when customers are active. Order counts cannot confirm it. Test it by sending the same email to two randomly chosen halves of your list at different times, and compare the results.

Testing and Refinement

Run the test for several weeks and track these against a control group that keeps your old timing:

How much timing helps differs by list and product, and for some it does not help at all. Record your baseline before you change anything, so the result can be read against it.

For deeper insights into campaign optimization, explore AI-driven analysis approaches.

Step 4: Are There Weekend vs Weekday Patterns?

The final step compares weekend and weekday ordering. Compare orders per day, not totals, because the weekdays are five of the seven days.

Segmenting Weekend and Weekday Data

import numpy as np

days_in_range = pd.date_range(orders["local"].min().normalize(), orders["local"].max().normalize(), freq="D")
n_weekend_days = int((days_in_range.dayofweek >= 5).sum())
n_weekday_days = len(days_in_range) - n_weekend_days

is_weekend = orders["dow"] >= 5
wk, we = orders[~is_weekend], orders[is_weekend]

print(f"Weekday: {len(wk)} orders, {len(wk) / n_weekday_days:.1f} per day, peak hour {wk['hour'].value_counts().idxmax():02d}:00, mean order value ${wk['amount'].mean():.2f}")
print(f"Weekend: {len(we)} orders, {len(we) / n_weekend_days:.1f} per day, peak hour {we['hour'].value_counts().idxmax():02d}:00, mean order value ${we['amount'].mean():.2f}")

# Order value gap with a 95% bootstrap interval, so the gap is not read as exact.
rng = np.random.default_rng(7)
diffs = [rng.choice(we["amount"], len(we)).mean() - rng.choice(wk["amount"], len(wk)).mean() for _ in range(5000)]
lo, hi = np.percentile(diffs, [2.5, 97.5])
print(f"Weekend minus weekday mean order value: ${we['amount'].mean() - wk['amount'].mean():.2f} (95% bootstrap interval ${lo:.2f} to ${hi:.2f})")

Expected Output

Weekday: 990 orders, 16.5 per day, peak hour 12:00, mean order value $89.13
Weekend: 187 orders, 7.8 per day, peak hour 13:00, mean order value $106.34
Weekend minus weekday mean order value: $17.21 (95% bootstrap interval $9.82 to $24.82)

Understanding the Patterns

Why weekend orders are larger is not something the data says (in this made-up data the gap was built into the generator). It could be what people buy, who buys, or a promotion that ran on weekends, so test any explanation before building a strategy on it.

Expected Outcome

You should now see how your own weekday and weekend ordering differ in volume per day, in peak hour and in order value, and how wide the uncertainty on the order value gap is.

Interpreting Your Results

Now that you've completed your analysis, it's important to contextualize your findings and avoid common interpretation pitfalls.

Context Matters

Your order timing patterns don't exist in isolation. Consider these contextual factors:

Statistical Considerations

Ensure your insights are based on adequate data:

Combining with Other Analytics

Order timing analysis becomes even more powerful when combined with other analytical approaches. Consider integrating insights from survival analysis techniques to understand customer lifetime patterns or ensemble methods to predict optimal timing for individual customer segments.

Reading the Concentration

Not every pattern needs action. Compare each share with what an even spread would give: 100% divided by 7 is 14.3% per day and 100% divided by 24 is 4.2% per hour. In the example, Wednesday at 18.9% and the top three hours at 25.2% (against 12.5% for an even spread) are above an even spread; a distribution close to even is a weak pattern, and a test of different timing is a better use of effort than following it.

Run It on Your Own Shopify Export

Export your orders from Shopify as a CSV and upload it at MCP Analytics. The analysis runs an order timing analysis by hour and day on your file and shows the result with the method used. You choose the columns; nothing connects to your Shopify account.

Try it on your data

Common Issues and Solutions

Here are solutions to the most common challenges encountered during order timing analysis:

Issue: Flat or No Clear Pattern

Symptoms: Orders are distributed relatively evenly across days and hours with no clear peaks.

Solutions:

Issue: Inconsistent Week-to-Week Patterns

Symptoms: Peak days and times vary significantly from week to week.

Solutions:

Issue: Multiple Time Zones Skewing Results

Symptoms: Hourly patterns show multiple peaks or unusual distributions.

Solutions:

Issue: Recent Pattern Shifts

Symptoms: Historical patterns don't match recent (last 4-6 weeks) behavior.

Solutions:

Issue: Low Order Volume Making Analysis Difficult

Symptoms: You have few orders in total, so the patterns can move a lot from week to week.

Solutions:

Issue: Data Export or Integration Problems

Symptoms: Unable to export data or read orders through the API.

Solutions:

Next Steps with Shopify Analytics

Congratulations! You've completed a comprehensive order timing analysis. Here's how to build on this foundation:

Immediate Actions (This Week)

  1. Set Up an Email Timing Test: Send your next email to two random halves of your list, one in your candidate window and one at your usual time
  2. Note an Ad Schedule Test: If you run ads, write down the candidate window as a schedule to test against your current one
  3. Plan Your Next Promotion: Pick a candidate window to test, since the peak day and hour are not settled by order counts alone
  4. Brief Your Team: Share insights with marketing and customer service teams

Medium-Term Optimizations (Next Month)

  1. A/B Test Timing: Test your optimal timing hypothesis against your previous schedule
  2. Segment-Specific Timing: Analyze whether different customer segments have different patterns
  3. Content Calendar: Build a 3-month content calendar based on timing insights
  4. Inventory Planning: Ensure popular products are well-stocked before peak days

Advanced Analyses to Explore

Continuous Improvement

Order timing analysis isn't a one-time activity. Schedule regular reviews:

Related Resources

Expand your Shopify analytics capabilities with these complementary analyses:

Conclusion

Order timing analysis turns vague intuitions about customer behavior into counts you can test. By seeing when orders arrive, you can choose candidate times for email sends, advertising schedules and promotions, and then test them.

The examples and patterns shown in this tutorial provide a framework, but your specific results will be unique to your store, products, and customer base. The key is to approach this analysis systematically, interpret results in context, and continuously refine your strategies based on performance data.

Remember that timing optimization is just one component of a successful e-commerce strategy. Combine these insights with strong product offerings, compelling marketing messages, excellent customer service, and competitive pricing to maximize your Shopify store's potential.

Start with your own orders: count them by day and hour in your shop's time zone, check that the pattern holds in both halves of your data, and test a change before you rely on it.

Sources

Explore more: Shopify Analytics: all tools, tutorials, and guides →

Not sure which plan? Compare plans →

Your turn

Bring your own data and the question you actually need answered.

MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai