The week signups fell 27% and revenue hit a high
Weekend signups dropped and I went in expecting an outage. It was the seasonal downswing of the whole market, and once I pulled the cohorts it turned out this was never a retention business.
Weekend signups dropped noticeably. I went in assuming something had broken, either in the product or in search traffic, and the answer was neither. The market itself was on its way down for the season, and while I was in there measuring cohorts, one of the premises of this business changed.
The app, a mock test for OPIc, the English speaking exam, makes its money on credits. You get free credits on signup, and you buy credits for AI scoring.
The first query showed a total outage
The first thing I did was pull daily signups from the database. The result was far worse than I expected. It wasn't a dip. From a certain date the signups looked almost cut off. I wondered whether the signup path had died, and for a moment I genuinely believed it was an outage.
It wasn't an outage. I'd written the query wrong.
This app's ORM stores DateTime as UTC without a timezone. If you put AT TIME ZONE 'Asia/Seoul' on that column, Postgres treats the value as if it were already Seoul time and converts it back to UTC. That's the opposite direction from the correct one, so the two are 18 hours apart. The most recent eighteen hours of signups slide into the previous day, today's cell looks empty, and over a few days it reads as if signups had stopped.
-- wrong: the value is timestamp (without tz), so it is treated as Seoul time and converted to UTC
created_at AT TIME ZONE 'Asia/Seoul'
-- right: it is just a UTC value plus nine hours
created_at + interval '9 hours'It wasn't the first time I'd hit this trap. I ran into exactly the same thing writing a cost-aggregation query for the same app, and I wrote it down in my notes at the time. Only after fixing the query did the real number come out.
The real number was −27%
| Period | Signups (Mon–Sat) |
|---|---|
| Sept 7 – 12 | 114 |
| Sept 14 – 19 | 83 |
A 27% drop. Not an outage, but not a number to shrug at either. At this size you usually suspect one of two things. Either search rankings slipped, or some screen broke.
So I opened Search Console. Daily clicks were 69 to 73 between September 8th and 10th, and 35 to 50 between the 13th and 17th. The same shape as signups. Position per page was unchanged; what had fallen was impressions.
Fewer impressions can mean fewer people searching. So next I looked at Google Trends for 오픽, the Korean name of the exam. September 6th to 10th sat at 100, and after that it had come down to around 45.
Looking at our own metric alone, it seems like something happened to us, but search volume itself was moving in that shape. There was nothing to fix in the app.
Five years of search volume
I stretched Trends out to five years. Every year, search volume for the exam spikes sharply in March–April and in early September, then falls back to baseline within two to four weeks. The 2026 September peak was the 6th to the 10th, and the weekend where I felt "signups have dropped" was exactly the downslope right after it.
I think the driver is hiring season. The exam is offered year-round, so the question "was last week an exam week" doesn't even make sense. My guess is that application deadlines are what send people to the search bar. That's only an interpretation, though, so I can't be sure.
Then let's hold on to the people who came in September
Once you're here, there's a natural next thought. If we could hold the people who came in during the peak until the next peak in March–April, the next peak could start with returning users. That was the direction I set on the day I started the diagnosis. Raise retention, flatten the seasonality.
So I measured the cohorts. For each signup week, how many people used AI scoring at least once the following week.
The signup line climbs toward the peak while the active line stays glued to the floor. As percentages: 11%, 7%, 10%, 2%, 0%. The peak-week cohort of September 7th brought in 124 people and 3 of them used it the following week. Of the 202 people in the peak cohort from September 1st to 12th, 6 were active in the last seven days. Three percent.
That number is where I dropped the direction. Before even thinking about how to carry 3% six months forward, there was nobody to hold. Once you've sat the exam, the purpose is gone, and that's why people leave. Had MAU or retention been the KPI, I'd have kept chasing the wrong thing.
It was never a retention business. So what was it?
Payment happens within three days
I pulled how many days pass between signup and the first payment.
Same day plus one to three days is 68% of the total. Only five payments took more than eight days.
68% within three days. And this lag completes the answer to the original question.
In the week of September 14th, when signups fell 27%, revenue was actually the highest yet. On web payments, 78,200 won across 10 payments. The week before, 74,700 won across 13. The cohort that came in during the peak paid a few days later, and since that lag is around three days, it landed in the following week's revenue. I found that a week where signups fall and revenue rises is simply the normal shape of this business.
So the formula shrinks to one line. Revenue is new signups during the peak, multiplied by conversion to payment within three days. Active users outside the peak don't appear in it.
What to do in each phase
Once it was laid out this way, the year's work split by season.
| Phase | Do | Don't |
|---|---|---|
| Rise (2–4 weeks before peak) | Prepare a deploy freeze, press the payment path for real | Big changes |
| Peak (Mar–Apr, early Sept) | Conversion only. Watch for payment and login failures | Feature releases |
| Fall (2–4 weeks after peak) | Post-mortems, stability work | Shaking the product to find a cause |
| Base (off-season) | Build SEO and store assets. They take 4–8 weeks to work, so build here | Retention improvement projects |
The most expensive item on this table is the payment check during the rise. Right before this September's peak, Android in-app payments were silently dead for ten days, and I wrote about that separately. At the time it ended as "I didn't verify". Seen from here, those ten days came right before one of only two revenue windows in the year.
The off-season work changes too. A search asset takes four to eight weeks after you build it before it does anything. Build it during the peak and it's late. Build it during the fall and you get impatient. Something built in October through December should start working in time for the March peak, so I'm treating the next three months as what decides the next peak.
Not yet tested
That's the set of principles I wrote down on September 20th. None of it has been tested yet. The next peak is March–April, so the result won't be in for half a year. If it turns out then that assets built in the off-season raised peak signups, this post was right. If not, it's a post that used seasonality as an excuse to let six months slide.
The seasonal curve is someone else's data, too. Right now Google Trends is the reference, and whether our own signup curve shows the twice-a-year spike will take the next peak to find out. I've decided to log the three series together once a month, and I think that after the next peak passes we'll be able to read the phase without Trends.
And this wasn't the first time on the timezone trap. It was in my notes and I stepped on it anyway, so there's no guarantee I won't step on it again. This time I left the correct query behind in a read-only script, but the problem is the day I write a new query by hand.
The next check is the first Monday of October. Until then, I've decided not to touch anything even if the numbers go down.
End