RisingWave computes ARPPU (Average Revenue Per Paying User) in real time by streaming transaction events through continuously maintained materialized views. Monetization teams get live ARPPU breakdowns by game, region, platform, and product tier—without waiting for nightly batch jobs to complete.
What Is ARPPU and Why Does Real-Time Matter?
ARPPU is one of the most important monetization KPIs in gaming. Unlike ARPU (which averages revenue across all players including non-payers), ARPPU isolates the value generated by paying users specifically. It answers: "Among players who choose to spend, how much are they spending on average?"
A rising ARPPU with flat DAU might mean your whales are spending more—or that casual spenders are churning, leaving only high-spenders in the denominator. A falling ARPPU after a price change reveals that the new pricing alienated your spending base.
These signals matter in hours, not days. When a new monetization event launches—a limited skin, a flash sale bundle, a holiday offer—you need live ARPPU data to know if it's working. RisingWave provides this through streaming SQL materialized views that update continuously as purchases arrive.
Ingesting Transaction and Session Data
CREATE SOURCE transactions (
transaction_id VARCHAR,
player_id BIGINT,
game_id VARCHAR,
product_id VARCHAR,
product_tier VARCHAR, -- 'starter', 'standard', 'premium', 'ultimate'
usd_amount DOUBLE PRECISION,
platform VARCHAR,
region VARCHAR,
is_refund BOOLEAN,
purchase_time TIMESTAMPTZ
)
WITH (
connector = 'kafka',
topic = 'transactions',
properties.bootstrap.server = 'kafka:9092',
scan.startup.mode = 'latest'
)
FORMAT PLAIN ENCODE JSON;
Core ARPPU Materialized View
ARPPU = Total Revenue / Number of Paying Users. This view computes it across hourly windows, segmented by game, platform, and region:
CREATE MATERIALIZED VIEW arppu_hourly AS
SELECT
window_start,
window_end,
game_id,
platform,
region,
COUNT(DISTINCT player_id) AS paying_users,
SUM(usd_amount) FILTER (WHERE is_refund = false) AS gross_revenue,
SUM(usd_amount) FILTER (WHERE is_refund = true) AS refund_amount,
SUM(usd_amount) FILTER (WHERE is_refund = false)
- ABS(SUM(usd_amount) FILTER (WHERE is_refund = true))
AS net_revenue,
ROUND(
(SUM(usd_amount) FILTER (WHERE is_refund = false))
/ NULLIF(COUNT(DISTINCT player_id), 0),
2) AS arppu,
COUNT(*) AS total_transactions,
ROUND(
COUNT(*)::NUMERIC / NULLIF(COUNT(DISTINCT player_id), 0),
2) AS transactions_per_payer
FROM TUMBLE(transactions, purchase_time, INTERVAL '1 hour')
WHERE usd_amount != 0
GROUP BY window_start, window_end, game_id, platform, region;
This single view gives your monetization team live ARPPU by platform and region—updated every hour as the window slides forward.
Rolling 7-Day ARPPU for Trend Analysis
Hourly ARPPU is noisy. For trend detection, a rolling 7-day window smooths out day-of-week effects and gives a cleaner picture of monetization health:
CREATE MATERIALIZED VIEW arppu_rolling_7d AS
SELECT
game_id,
platform,
window_end::DATE AS snapshot_date,
COUNT(DISTINCT player_id) AS paying_users_7d,
SUM(usd_amount) FILTER (WHERE is_refund = false) AS revenue_7d,
ROUND(
SUM(usd_amount) FILTER (WHERE is_refund = false)
/ NULLIF(COUNT(DISTINCT player_id), 0),
2) AS arppu_7d,
AVG(usd_amount) FILTER (WHERE is_refund = false) AS avg_transaction_value,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY usd_amount)
FILTER (WHERE is_refund = false) AS median_transaction_value
FROM HOP(transactions, purchase_time, INTERVAL '1 day', INTERVAL '7 days')
WHERE usd_amount > 0
GROUP BY game_id, platform, window_end;
The 7-day rolling ARPPU is your primary trend line. The gap between avg_transaction_value and median_transaction_value reveals whale skew—when your top 5% of spenders are pulling the average far above the median, your monetization is whale-dependent.
ARPPU by Product Tier
Understanding which product tiers drive ARPPU helps optimize your catalog:
CREATE MATERIALIZED VIEW arppu_by_product_tier AS
SELECT
window_start,
window_end,
game_id,
product_tier,
COUNT(DISTINCT player_id) AS buyers,
SUM(usd_amount) FILTER (WHERE is_refund = false) AS tier_revenue,
ROUND(
SUM(usd_amount) FILTER (WHERE is_refund = false)
/ NULLIF(COUNT(DISTINCT player_id), 0),
2) AS tier_arppu
FROM TUMBLE(transactions, purchase_time, INTERVAL '1 day')
WHERE usd_amount > 0 AND is_refund = false
GROUP BY window_start, window_end, game_id, product_tier;
Comparison: ARPPU Monitoring Approaches
| Dimension | Manual BI Reports | Data Warehouse Refresh | Streaming (RisingWave) |
| Update frequency | Daily/weekly | Hourly at best | Per transaction |
| Regional breakdown | Aggregated | Available but delayed | Live, per-hour per-region |
| Whale skew detection | Post-hoc | Delayed | Real-time median/mean gap |
| Flash sale effectiveness | Next-day report | Hours later | Live conversion tracking |
| Platform comparison | Manual exports | ETL-dependent | Always-current |
| Refund impact | Reconciled monthly | Delayed | Netted in real time |
Sinking ARPPU to Your BI System
CREATE SINK arppu_to_analytics_db
FROM arppu_rolling_7d
WITH (
connector = 'jdbc',
jdbc.url = 'jdbc:postgresql://analytics-db:5432/monetization',
table.name = 'arppu_rolling_7d',
type = 'upsert',
primary_key = 'game_id,platform,snapshot_date'
);
Your existing Grafana dashboards now show always-current 7-day ARPPU without any pipeline changes—just point them at the analytics database that RisingWave keeps updated.
Monetization Signals to Watch
ARPPU divergence by platform: If iOS ARPPU is running 40% above Android, it's either audience mix or price sensitivity. Stream both to live dashboards and correlate with A/B test variants running on each platform.
ARPPU vs. paying user count: Healthy monetization grows both. If ARPPU rises while paying user count falls, you're extracting more from fewer spenders—a warning sign for long-term sustainability.
Regional ARPPU for pricing strategy: Lower-income regions often show lower absolute ARPPU but higher transaction counts. Use live regional ARPPU to validate localized pricing experiments.
Post-event ARPPU decay: After a special event ends, watch ARPPU decay rate. Fast decay suggests players were event-motivated, not permanently monetizable. Slow decay indicates the event converted players into habitual spenders.
FAQ
Q: How is ARPPU different from ARPU, and when should I track each? A: ARPU (Average Revenue Per User) divides total revenue by all active users—free and paying. ARPPU divides by paying users only. Track ARPU for overall monetization efficiency relative to your player base. Track ARPPU to understand spender behavior and optimize for your paying audience.
Q: Can I track ARPPU by player cohort (e.g., players who started in Q1)?
A: Yes. Store player cohort data in a player_cohorts reference table, then join transactions with cohort assignments in your materialized view. This gives you cohort-adjusted ARPPU—essential for comparing monetization across acquisition campaigns.
Q: How do refunds affect the ARPPU calculation?
A: The arppu_hourly view uses is_refund = false to compute gross revenue and separately tracks refund amounts. The net_revenue column subtracts refunds. Query net_revenue / paying_users for net ARPPU, which is what you should report to finance.
Q: What's a good ARPPU benchmark for mobile games? A: Industry benchmarks vary widely by genre. Casual mobile games typically see $5-15 monthly ARPPU. Mid-core and strategy games: $20-50. MMORPGs and hardcore titles: $50-200+. RisingWave helps you track your own ARPPU trends and measure the impact of changes—the absolute number matters less than the direction.
Q: Can I set up automated alerts when ARPPU drops below a threshold?
A: Yes. Create a Kafka sink from arppu_rolling_7d. Your alerting service consumes this and compares against a threshold table. Alternatively, query the view directly from Grafana Alerting using a PostgreSQL data source with a threshold rule.
Get Started
Monitor ARPPU in real time and make smarter monetization decisions:
- Documentation: docs.risingwave.com/get-started
- Community: Join the conversation on RisingWave Slack

