ActorStack.dev

One proposal in the first hour, ten by the twelfth

Measured across 401 projects: competition on a freelance listing has a half-life measured in hours, which makes a stale row a wrong row.

By Oswaldo Carabano6 min read

Short answer

Measured across 401 Workana projects, the median proposal count moves from about 1 in the first hours after posting to about 10 by the twelfth hour. If you are using `total_bids` to judge competition, a row cached for a day is describing a different project from the one on the site — which is why the Actor's default cache age is 6 hours and every row declares its own age.

Key points

  • Median proposals: about 1 in the first hours, about 10 by hour twelve, measured across 401 projects.
  • `total_bids` is therefore a time-sensitive field in a way that budget and skills are not.
  • The default cache age is 6 hours, which is a deliberate compromise rather than a round number.
  • `posted_age_hours` on every row lets you normalise competition by age instead of comparing raw counts.
  • For live bidding decisions, `maxCacheAgeHours: 0` is the correct setting and costs more.
On this page6 sections

If you are deciding whether to bid on a project, the number that matters is how many people already have. That number has a half-life measured in hours, which makes it the one field in this dataset where cache age is a correctness issue rather than a cost trade-off.

The measurement

Across 401 projects, the median proposal count goes from about 1 proposal in the first hours after posting to about 10 by the twelfth hour.

Twelve hours is roughly the window in which a project goes from uncontested to crowded. That is faster than most people assume, and it is the reason the Actor's default cache age is 6 hours rather than a day.

Why a stale row is a wrong row

Most fields on a project do not move: the title, the budget, the skills and the country are the same tomorrow. total_bids is not — a row cached for 24 hours is describing a different competitive situation from the one on the site.

The failure is quiet. Your dataset shows a project with 2 proposals; you spend an hour writing a proposal; the project has 22. Nothing errored, and the number you acted on was real when it was fetched.

Normalising by age instead of comparing counts

Raw proposal counts are not comparable across projects of different ages, which is what posted_age_hours is for:

proposals per hour, and the age band
SELECT
  slug, title, total_bids, posted_age_hours,
  ROUND(total_bids::numeric / GREATEST(posted_age_hours, 1), 2) AS bids_per_hour,
  CASE
    WHEN posted_age_hours < 6  THEN 'fresh'
    WHEN posted_age_hours < 24 THEN 'today'
    ELSE 'older'
  END AS age_band
FROM projects
WHERE data_age_hours < 2
ORDER BY bids_per_hour ASC;

Sorting ascending by proposals per hour is the actual opportunity list: projects that have been up long enough to be real and have attracted less competition than their age would predict.

Why the default cache is 6 hours

It is the halfway point in the window where the number changes most. Shorter and most analytical runs pay for freshness they do not need; longer and a bidding decision is made on a number that has doubled.

Every row declares from_cache, fetched_at and data_age_hours regardless, so the choice is visible in the data — the general principle.

Running live

maxCacheAgeHours: 0 forces a fresh read of every row. It costs more in requests and wall-clock time, and for a bidding workflow it is the only correct setting.

For demand analysis — budgets by skill, subcategory mix, country comparisons — leave the default. None of those fields move within a day, and paying for freshness you do not use is the mirror image of the mistake above.

The opportunity this exposes

If competition arrives within hours, then latency is a competitive advantage on this platform — which is a slightly uncomfortable conclusion to publish, and it follows from the measurement.

The honest framing: a freelancer who sees new projects in their skill within the first hour is bidding against one or two people instead of ten. That is what the data says. Whether a market where speed beats fit is a good market is a different question, and not one the data can answer.

Frequently asked questions

How fast do proposals arrive on Workana?
Measured across 401 projects, the median goes from about 1 proposal in the first hours to about 10 by the twelfth. Competition arrives faster than most people assume, which is why row freshness matters for this field specifically.
How do I compare competition across projects?
Divide by age, or bucket by it. `posted_age_hours` is on every row, and a project with 8 proposals at two hours old is far more contested than one with 12 at three days.
Should I set the cache age to zero?
If you are bidding, yes — a stale proposal count is a wrong one. If you are analysing demand by skill or budget, the 6-hour default is fine and cheaper, because those fields do not move.

Sources

Every URL below was requested and returned a page on the date shown.

  1. Operator claimchecked 19 Aug 2026
    Workana Projects & Client Demand Scraper — Actor README and input schemaActorStack / Apify Store
A laptop showing lines of code on a wooden desk in a dimly lit room.
WorkanaGuide

Scrape Workana

A walkthrough of extracting LATAM freelance demand: the five filters that work, the coverage ceiling nobody can exceed, and why the language setting changes what a budget means.

9 min
Aerial view of a Latin American city centre at night, streets picked out in light.
WorkanaGuide

Market analysis

What listing data supports — demand by skill, budget distributions, competition levels — and the four claims it cannot carry.

8 min
A white measuring tape curving across a dark background, showing the numbers 15 to 45.
WorkanaMeasured

The rating that is not a rating

Workana returns a rating for every project, so "100% coverage" would be true. It would also throw away four fifths of the market if you believed it.

6 min