Use case

How to detect a hiring freeze from a company's job postings

A hiring freeze stops the flow of new roles and leaves the open ones to age. The counts to compare, five queries, a table of look-alikes and a worked example.

11 min read

A hiring freeze shows on a company's careers page as a pattern you can measure: the flow of new roles stops while the roles already open stay up. This guide shows how to detect a hiring freeze from a company's job postings. It gives the signals, the queries on Job Postings and Hiring Activity that compare each with the company's own earlier weeks, the innocent explanation behind every pattern and a worked example to redo on the companies you track.

How to detect a hiring freeze from the signals in a company's job postings

To detect a hiring freeze from a company's job postings, compare the flow of new roles with the company's own earlier weeks. Three signals together mark a freeze: new postings fall to near zero, open postings stay up and grow older, and the few roles that still appear are replacements. One signal alone is ordinary noise.

Where a freeze is written down in public, its terms show what to look for. The United States federal hiring freeze of 20 January 2025 ordered that no vacant civilian position be filled and no new position be created, and it did not apply to military personnel or to positions related to immigration enforcement, national security or public safety. Read a company's board for the same three things: vacancies left standing, no new roles, and a short list of exceptions.

Three counts in Hiring Activity (company_hiring_daily) carry the measurement, on one row for each company and closed day. New postings are those seen opening that day, closed postings are those closed that day, and open postings are those open at the end of it. A posting already published when a board is first observed sets the baseline and is not counted as new. New postings are the flow that a freeze stops, and open postings are the stock, which in a freeze holds. A stock that falls is a cut, and detecting restructuring early from postings and announcements covers that screen and the disclosures that confirm it.

New postings against the company's own earlier weeks

The query cuts the days up to an as-of date into 28-day windows, numbered back from zero, so that every window holds four of each weekday. It returns the companies whose latest window has at most a quarter of their usual new postings and fewer than any earlier window.

You set that date through the parameter :as_of, which later queries reuse: bind it, or write a date in its place. The query compares the latest window with the complete windows before it and needs at least three of them. Like the other queries here, it is written for PostgreSQL.

with w as (
  select
    company_id,
    (cast(:as_of as date) - day) / 28 as window_no,
    sum(new_postings)    as new_28d,
    sum(closed_postings) as closed_28d,
    count(*)             as days
  from company_hiring_daily
  where day <= cast(:as_of as date)
  group by company_id, window_no
),
base as (
  select
    company_id,
    percentile_cont(0.5) within group (order by new_28d) as usual_new_28d,
    min(new_28d)                                         as quietest_new_28d,
    count(*)                                             as earlier_windows
  from w
  where window_no >= 1
    and days = 28
  group by company_id
)
select
  w.company_id,
  w.new_28d,
  w.closed_28d,
  b.usual_new_28d,
  b.quietest_new_28d,
  b.earlier_windows,
  o.open_postings
from w
join base b on b.company_id = w.company_id
join company_hiring_daily o
  on  o.company_id = w.company_id
  and o.day = cast(:as_of as date)
where w.window_no = 0
  and b.earlier_windows >= 3
  and b.usual_new_28d >= 6
  and w.new_28d <= 0.25 * b.usual_new_28d
  and w.new_28d <  b.quietest_new_28d
order by b.usual_new_28d desc;

The yardstick, usual_new_28d, is the median of the company's earlier complete windows, so each company is measured against its own record. Set :as_of to a closed day: daily rows are written once, after the day closes. The quarter, the floor of six and the three earlier windows are starting points to tune on the companies you track.

The floor is there because small counts go quiet by chance. Assume that new postings arrive independently at a steady rate. A company that usually opens three in 28 days then shows an empty window about one time in twenty, and one that usually opens six about one time in 400. A board that publishes in batches has more empty windows than a steady rate predicts, which is why the query also asks for a window quieter than every earlier one.

When job openings disappear from the postings: recruiting freeze signals

Job openings that disappear from a company's postings are a recruiting freeze signal only when nothing replaces them. Roles are filled or withdrawn every week, so closings alone say little. The pattern to look for is a run of closings with no new postings beside it, or a block of closings that no new roles follow.

Closings come in two shapes. The first is a slow run. In a freeze that leaves its listings standing, closings fall with openings, because a role nobody is hired into stays on the board. Closings well below the company's usual level, beside new postings near zero, fit a freeze.

The second shape is a block: one day whose closings are a large share of the open postings. A company that withdraws its open roles when it freezes produces one. So do three ordinary events.

  • A shared expiry date. Oracle's recruiting documentation describes a posting schedule with a start date and an expiration date. When the expiration date is reached, the requisition is removed from the career sites. Listings published together for the same term leave together.
  • An intake that ends. A graduate or seasonal intake that opens many listings together also closes them together, while new postings in other functions carry on.
  • A rebuilt board. A company that moves to another applicant tracking system empties one board and fills the next. The old listings close in a block and the same roles return as baseline postings, which are not counted as new.

The query finds the blocks and shows what followed each one.

with d as (
  select
    company_id,
    day,
    closed_postings,
    lag(open_postings)      over w as open_before,
    lead(open_postings, 14) over w as open_14d_later,
    sum(new_postings) over (
      partition by company_id
      order by day
      rows between 1 following and 14 following
    ) as new_next_14d
  from company_hiring_daily
  window w as (partition by company_id order by day)
)
select company_id, day, closed_postings, open_before, new_next_14d, open_14d_later
from d
where open_before >= 20
  and closed_postings >= 0.2 * open_before
  and open_14d_later is not null
order by day desc;

Read the last two columns. New postings in the 14 days after a block are turnover. Open postings back near open_before with new_next_14d at zero are a rebuilt board, because the roles returned as baseline postings. A block followed by neither is a set of roles withdrawn and not replaced.

Signs of a hiring freeze in job listings: stale and reposted roles

Stale job listings are a sign of a hiring freeze when the whole board ages together: no new roles arrive, and the median age of the open listings rises by about a day for each day that passes. A reposted listing points the other way. An employer that republishes a role is still recruiting for it.

A listing and an opening are different things. The United States Bureau of Labor Statistics counts a job as open only when three conditions hold: a specific position exists with work available, the job could start within 30 days, and the employer is actively recruiting from outside the establishment, which can include posting notices on the internet. A listing is evidence of the third and says nothing of the first two.

Some listings stand with no opening behind them by design. Oracle's documentation describes a pipeline requisition for jobs an organisation is often hiring, which stays open for applications when no opening exists. Search rules push the other way: Google's documentation for job posting markup says jobs no longer open for applications must be expired, by a valid-through date in the past, by removing the page or by removing the markup. So one old listing proves little, and a board that ages as a whole proves a good deal.

The query gives the age of every company's open listings on one day.

select
  company_id,
  count(*) as open_postings,
  percentile_cont(0.5) within group (
    order by cast(:as_of as date) - cast(coalesce(posted_at, first_seen_at) as date)
  ) as median_age_days,
  avg(case
        when cast(:as_of as date) - cast(coalesce(posted_at, first_seen_at) as date) > 90
        then 1.0 else 0.0
      end) as share_over_90_days
from job_postings
where cast(first_seen_at as date) <= cast(:as_of as date)
  and (closed_at is null or cast(closed_at as date) > cast(:as_of as date))
group by company_id;

In Job Postings (job_postings) posted_at is the date the board gives and first_seen_at is the first observation of the posting. Where a posting carries the flag found_on_first_read and no posting date, its age is a minimum. Run the query for two days 28 days apart. With no new listings and no closings every open listing is 28 days older, so the median rises by exactly 28. Each new listing pulls it back, so a rise of more than 20 days is a board ageing nearly a day for each day. A high share over 90 days beside a steady median is a set of standing listings.

A role taken down and published again as a new listing is two records: one closed, and one first seen soon after with the same title and country. In Hiring Activity the pair adds one closing and one new posting and leaves open postings level. This query lists the pairs for the latest window.

select
  n.company_id,
  n.posting_id as new_posting,
  c.posting_id as closed_posting,
  n.title,
  n.country,
  c.closed_at,
  n.first_seen_at
from job_postings n
join job_postings c
  on  c.company_id  = n.company_id
  and c.title       = n.title
  and c.country     = n.country
  and c.posting_id <> n.posting_id
  and c.closed_at is not null
  and c.last_seen_at <= n.first_seen_at
  and c.last_seen_at >  n.first_seen_at - interval '14 days'
where not n.found_on_first_read
  and n.first_seen_at >= cast(:as_of as date) - 27
  and n.first_seen_at <  cast(:as_of as date) + 1;

A repost says two things. The role is still advertised, and posting a notice is recruiting in the Bureau's terms, so recruiting has not stopped. It is also not a new role, so take the pairs out of new postings in every window before you compare the latest with the earlier ones.

Which roles still open: the mix in a partial freeze

A freeze with exceptions leaves a trickle of new postings, and their labels say what kind of roles they are. Each record of Job Postings carries a job function, a seniority and ten role flags. Two of the flags point to growth: team build, for one of several hires growing a team, and new initiative. The query counts, for each window, the new postings and those that carry either flag.

select
  company_id,
  (cast(:as_of as date) - cast(first_seen_at as date)) / 28 as window_no,
  count(*) as new_postings,
  sum(case
        when labels ->> 'team_build' = 'true'
          or labels ->> 'new_initiative' = 'true'
        then 1 else 0
      end) as growth_postings
from job_postings
where not found_on_first_read
  and labels ->> 'team_build' is not null
  and cast(first_seen_at as date) <= cast(:as_of as date)
group by company_id, window_no
order by company_id, window_no;

A window whose few new postings carry neither flag, after windows in which many did, reads as hiring kept to replacements. The filter on team_build keeps the postings whose labels carry both flags. Read a count, not a share, when a window holds fewer than ten postings. Group the same query by the function label to find a freeze that covers some functions and spares others.

Patterns that look like a hiring freeze and the innocent explanation of each

Every pattern above has an ordinary cause, and the table sets each beside the test that separates the two.

PatternIn the countsInnocent explanationThe test
New postings stop, old ones stay openNew postings near zero, open postings levelA small employer between hiresA usual level of six or more, a window quieter than every earlier one, then a second window
Listings go staleOld listings on the boardA standing listing kept open to collect applicationsThe median age of all open listings, on two days
Roles are repostedNew postings and closings rise together, open postings levelThe role is still being recruited forMatch title and country against recent closings and count the pairs out
Postings close in a blockOne day's closings are a fifth or more of the open postingsA shared expiry date, or an intake that endsNew postings in the 14 days after
A board is rebuiltA block of closings and no new postingsA move to another applicant tracking systemOpen postings recover, and the board system on the postings changes
The mix shifts to replacementsA trickle of new postings, none flagged as growthToo few postings to judgeTen or more postings before you read a share
One function goes quietNew postings stop in one job functionA team that is completeThe other functions, and the same function in the next window

A worked example: Acme Robotics

The figures are illustrative, for the invented company Acme Robotics.

Assumptions:

  • Six 28-day windows of Hiring Activity: five earlier and the latest.
  • 58 postings are open before the first window and no baseline postings arrive in the period, so open postings move only by new and closed postings.
  • The thresholds are those of the first query.
  • The median ages come from the age query, run on the last day of each window.
  • In the latest window the busiest day has two closings, and one of the three new postings matches a posting closed four days earlier with the same title and country.
  • Of the 104 new postings in the earlier windows, 47 carry the team-build or the new-initiative flag. None of the latest three does.
WindowNewClosedOpen at the endMedian age, days
Earlier 122196147
Earlier 218215850
Earlier 325226146
Earlier 420186347
Earlier 519186448
Latest395872

The working, test by test:

  1. Usual level. The five earlier counts of new postings sort to 18, 19, 20, 22 and 25. The median is 20 and the quietest window has 18.
  2. Flow. The latest window has 3 new postings, 15% of the usual 20. A quarter of 20 is 5, and 3 is below both 5 and 18, so the first query returns Acme Robotics.
  3. Stock. Open postings go from 64 to 58, since 64 + 3 - 9 = 58. That is a fall of 6, or 9%, so the stock holds.
  4. Age. The median age of the open postings rises from 48 days to 72, which is 24 days in a window of 28.
  5. Closings. The earlier windows sort to 18, 18, 19, 21 and 22, a median of 19. The latest has 9, under half of that, and no day with more than two: a slow run and no block.
  6. Mix. One of the three new postings is a repost, which leaves two new roles, and neither is flagged as growth. In the earlier windows 47 of 104 were, or 45%.

Acme Robotics has stopped opening roles and left its open listings to age, and its two new roles look like replacements: the pattern of a freeze with exceptions. The next window decides what follows. New postings near 20 end the freeze, and closings above openings with open postings falling turn it into a cut.

Reading the result

Call a freeze when the tests agree, in this order.

  1. The usual level clears the floor, with reposts taken out of every window.
  2. The latest window is at most a quarter of that level and quieter than every earlier one.
  3. Open postings hold. A fall of a quarter is a cut, and a different screen.
  4. The median age of the open postings rises by more than 20 days in 28.
  5. No block of closings, or one followed by neither new postings nor a recovery in open postings.
  6. The industry's series of new postings in Market Series has not fallen in step.
  7. A second window confirms the first.

A posting records an intention to hire, so the screen reads what a company advertises. A company that fills roles through agencies or referrals publishes few of them, and its board is read against its own level. Treat a hit as a reason to look.

A cut can carry a duty of notice. In the United States the WARN Act requires employers with 100 or more employees to give at least 60 calendar days' written notice of a plant closing and mass layoff affecting 50 or more employees at a single site, with certain exceptions. A freeze is a decision about roles not yet filled, and the company's own board is where it is recorded.

How Fokals delivers it

Job Postings and Hiring Activity belong to the hiring product, refreshed daily and delivered by REST API or as bulk files. Postings are sourced from each company's own board, each with its first-seen, last-seen and close dates. Daily rows are written once after the day closes and never revised, so the counts of a past window read the same whenever you run the first query, which is what a back-test on point-in-time data needs. The data dictionary defines every field used here.

Frequently asked questions

How long do new job postings have to stop before it is a hiring freeze?

There is no fixed length, because it depends on how often the company usually posts. A company that normally opens twenty roles in four weeks and opens three has given a clear signal in one 28-day window. One that normally opens three needs several quiet windows, since a single empty one happens by chance about one time in twenty if postings arrive at a steady rate. Ask for a window at most a quarter of the company's usual level and quieter than any earlier one, then wait for a second.

Does a stale job listing mean the company has stopped hiring?

Not on its own. Some listings stay open all year by design, to collect applications for roles a company often fills, and they are old on any board. What marks a freeze is the whole board ageing together: no new listings arrive, and the median age of the open ones rises by about a day for each day that passes. Compare the median age on two days four weeks apart before you read anything into one old listing.

Is a reposted job a sign of a hiring freeze?

No. A role that is taken down and published again is still being advertised, and advertising a role is recruiting. A repost does raise the count of new postings, though, because it looks like a new role. Match each new posting to a posting of the same title and country that closed in the days before it, and leave the matches out when you compare a window with the company's earlier weeks.

What is the difference between a hiring freeze and layoffs in job postings data?

A freeze stops the flow and keeps the stock: new postings fall to near zero while open postings hold and grow older. A cut reduces the stock: closings run above openings and open postings fall. A freeze can turn into a cut, so keep watching the open count after a freeze signal, and look for an announcement or a regulatory disclosure to confirm a cut.

Can job postings show which departments are under a hiring freeze?

Yes, where the company publishes enough roles to compare. Each record of Job Postings carries a job function label, so new postings can be counted by function for each 28-day window. A partial freeze shows as new postings stopping in some functions while they continue in others. Hiring Activity also gives the open postings by function for every closed day, which shows where the open roles are thinning.

The queries and code on this page are examples to adapt. Test them in your own environment before you rely on them.

What this page says about the products it names was checked against their public documentation on 7 October 2026. Product and company names are trademarks of their owners. Fokals is not affiliated with them or endorsed by them.